Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4391319
fix(sales): gate every runtime record/send surface against retired of…
nish3451 Aug 12, 2026
48ebb42
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
1b3721a
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
9eba605
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
cde0dbb
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
41dbe9f
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
37c1004
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
5bb3eb5
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
6e516c9
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
a024fe1
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
b16a33b
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 12, 2026
baef3fd
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 13, 2026
8ddf24a
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 13, 2026
0d056ad
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 13, 2026
c23b05f
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 13, 2026
f5900cd
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 13, 2026
4189e43
Merge branch 'main' into fix/outbound-retired-offer-runtime-gate-lane…
nish3451 Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions scripts/check-outbound-send-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,23 @@ try {
}

const { roots, strict } = config;
const outboundFiles = new Set(["next-message.md", "send-package.md", "recording-notes.md", "outreach.md", "reply-package.md", "call-booked-package.md", "close-package.md"]);
// Every active record/send surface, so a stale runtime sheet can never sell a
// retired broad-agency offer without a CI/operator failure. Mirrors the file
// set the proof-run brief tells the operator to record and send from.
const outboundFiles = new Set([
"next-message.md", "send-package.md", "recording-notes.md", "outreach.md",
"reply-package.md", "call-booked-package.md", "close-package.md",
"loom-outline.md", "recording-script.md", "recording-sharpness-brief.md",
"audit-brief.md", "buyer-room.md", "loom-package.md"
Comment on lines +84 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include sales-call prep in the retired-offer scan

When a legacy sales-call-prep.md still contains the retired sprint, send:check passes because this active sales-call close packet is omitted from outboundFiles, even though draft-sales-call-prep.mjs emits it for operators and check-outbound-claim-safety.mjs classifies it as outbound. Include this filename so the new “every active record/send surface” gate also blocks stale offer copy from being used on calls.

Useful? React with 👍 / 👎.

Comment on lines +82 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the Loom sheet in the retired-offer gate

When an existing prospects/loom-links.txt contains a retired ask and the operator starts the recording workflow without first rerunning market:proof-run, the export-only cleanup never executes. The proof-run instructions identify this sheet as the recording source, but this allowlist excludes it, so both send:check and the parity gate can pass while the operator records stale offer copy; scan the sheet's ask column here as well.

Useful? React with 👍 / 👎.

Comment on lines +82 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scan the generated teleprompter and outbox

When prospects/recording-teleprompter.html or prospects/outbox.html predates source normalization, this filename allowlist ignores the stale generated copy, so send:check passes even if those pages still contain a retired offer. This is directly user-facing because start-growth-mission.mjs opens the existing teleprompter for --view=record and the existing outbox for --view=send without regenerating either page; include these generated record/send surfaces in the retired-offer gate.

Useful? React with 👍 / 👎.

]);
const optOutPattern = /\b(reply no|do not follow up|unsubscribe|opt out|ignore me)\b/i;
const placeholderPattern = /\[(?:add Loom link|link|specific fault|Name)\]|Here is the Loom:\s*$/i;
const salesPlaceholderPattern = /\badd (?:meeting link|payment link|call time)\b/i;
// Retired broad-agency offers must never project into an outbound send package.
// Mirrors the canonical retired-ask guard in export-recording-rehearsal-check.mjs.
const retiredOfferPattern = /7[-\s]day (?:site|website) revenue (?:leak|fault) (?:fix )?sprint|tangible revenue (?:leak|fault) sprint|30[-\s]day action plan|growth desk|three pages|\$\s?500\b/i;
// Retired broad-agency offers must never project into an outbound send package
// or any runtime sheet that feeds it (loom outlines, recording scripts,
// sharpness briefs, audit briefs, buyer rooms). Mirrors the canonical
// retired-ask guard in export-recording-rehearsal-check.mjs.
const retiredOfferPattern = /7[-\s]day (?:site|website) revenue (?:leak|fault) (?:fix )?sprint|7[-\s]day sprint|tangible revenue (?:leak|fault) sprint|30[-\s]day action plan|growth desk|three pages|founder sprint|\$\s?500\b/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize worded seven-day promises

When a legacy runtime surface says “seven-day sprint” instead of “7-day sprint,” this new gate passes it unchanged because the added generic branch only accepts the numeral. The repository already classifies that spelling, plus phrases such as “fix in seven days,” as stale delivery promises in scripts/test-sales-intake-contract.mjs; use the same retired-promise variants here so send:check cannot approve those runtime files.

Useful? React with 👍 / 👎.


function walk(path) {
if (!existsSync(path)) return [];
Expand Down Expand Up @@ -127,7 +137,7 @@ for (const file of files) {
findings.push({ file, rule: "sales package still has meeting/payment placeholders" });
}

if (["send-package.md", "recording-notes.md"].includes(filename) && retiredOfferPattern.test(content)) {
if (retiredOfferPattern.test(content)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict retired-offer matching to actual offer sections

When a prospect's own pricing or page structure is copied into loom-outline.md, recording-sharpness-brief.md, or recording-script.md, this whole-file check treats descriptive phrases such as “three pages” or “$500” as though TinyStudio were selling the retired offer. These recording surfaces intentionally contain observed page cues and fault evidence, so an unrelated prospect statement can now fail send:check and the market-parity gate; inspect only the ask/CTA portions or require enough context to identify the retired offer.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude inactive prospects from the expanded scan

When a prospect has been marked won, lost, or paused but a historical loom-outline.md, recording-script.md, or other newly scanned file retains retired copy, this unconditional check still fails send:check and therefore the market-parity gate. The recording rehearsal, teleprompter, proof-run, and batch-send workflows explicitly exclude those stages, so these are no longer active record/send surfaces; filter prospect folders by pipeline status before applying the expanded scan.

Useful? React with 👍 / 👎.

findings.push({ file, rule: "outbound package sells a retired offer" });
}

Expand Down
21 changes: 20 additions & 1 deletion scripts/export-market-proof-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,25 @@ writeFileSync(resolvedOutputPath, markdown);

const loomLinksDir = resolvedLoomLinksPath.split("/").slice(0, -1).join("/");
if (loomLinksDir) mkdirSync(loomLinksDir, { recursive: true });
const existingLoomRows = read(resolvedLoomLinksPath).split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
// Retired broad-agency promise language must never persist in the proof-run
// sheet: it is the single source the brief tells the operator to record and
// send from. Existing rows keep their operator-captured fault/impact/fix
// notes and Loom URL, but any ask column that still names a retired offer is
// refreshed to the canonical ask. Mirrors the retired-offer pattern in
// check-outbound-send-readiness.mjs and export-recording-rehearsal-check.mjs.
const retiredPromisePattern = /7[-\s]day (?:site|website) revenue (?:leak|fault) (?:fix )?sprint|7[-\s]day sprint|tangible revenue (?:leak|fault) sprint|30[-\s]day action plan|growth desk|three pages|founder sprint|\$\s?500\b/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add token boundaries to the new generic alternatives.

/7[-\s]day sprint/ matches the 7-day sprint portion of 17-day sprint. /founder sprint/ matches cofounder sprint. In scripts/export-market-proof-run.mjs, this can replace a valid persisted ask. In the readiness checks, this can create an incorrect operator or CI failure.

Use \b7[-\s]day sprint\b and \bfounder sprint\b at every matcher site.

  • scripts/export-market-proof-run.mjs#L380-L380: bound the generic alternatives before migrating the ask field.
  • scripts/export-recording-rehearsal-check.mjs#L85-L90: bound the generic alternatives before reporting rehearsal findings.
  • scripts/test-active-offer-projection.mjs#L15-L15: use the bounded alternatives in the test helper.
  • scripts/check-outbound-send-readiness.mjs#L95-L95: bound the generic alternatives before blocking an outbound surface.
📍 Affects 4 files
  • scripts/export-market-proof-run.mjs#L380-L380 (this comment)
  • scripts/export-recording-rehearsal-check.mjs#L85-L90
  • scripts/test-active-offer-projection.mjs#L15-L15
  • scripts/check-outbound-send-readiness.mjs#L95-L95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/export-market-proof-run.mjs` at line 380, Update the retired-ask
matcher in scripts/export-market-proof-run.mjs:380-380,
scripts/export-recording-rehearsal-check.mjs:85-90,
scripts/test-active-offer-projection.mjs:15-15, and
scripts/check-outbound-send-readiness.mjs:95-95 to use token boundaries around
both the “7-day sprint” and “founder sprint” alternatives: apply \b before and
after each phrase while preserving the remaining patterns and behavior.

let refreshedAsks = 0;
const existingLoomRows = read(resolvedLoomLinksPath)
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
.map((line) => {
if (!line.includes("|")) return line;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh comma-delimited proof rows too

When an existing Loom sheet row uses the supported comma-delimited format, this early return skips retired-offer replacement even though parseLoomSheetLine, update-market-proof-looms.mjs, and prepare-prospect-batch-send.mjs all accept comma-separated rows. Regenerating the proof run therefore leaves the stale ask in the sheet that operators are instructed to record from; parse using the same delimiter selection as the consumers before inspecting and replacing the ask column.

Useful? React with 👍 / 👎.

const parts = line.split("|");
if (parts.length < 7 || !retiredPromisePattern.test(parts[6])) return line;
refreshedAsks += 1;
return [...parts.slice(0, 6), cleanSheetNote(canonicalProspectAsk(), parts[6])].join("|");
});
// Sheet row paths may be service-root relative or absolute; normalize both
// sides before deduplicating so regeneration never appends a duplicate row.
const sheetRowKey = (line) => {
Expand All @@ -394,5 +412,6 @@ console.log(JSON.stringify({
blockers: parity.blockers.map((blocker) => blocker.area),
selectedProspects: recordingBatch.map((prospect) => prospect.path),
directionGate,
refreshedAsks,
recommendedChannel: channelGuidance.recommendedChannel
}, null, 2));
2 changes: 2 additions & 0 deletions scripts/export-recording-rehearsal-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,12 @@ function bannedClaims(content) {

const RETIRED_ASK_PATTERNS = [
/7[-\s]day (?:site|website) revenue (?:leak|fault) (?:fix )?sprint/i,
/7[-\s]day sprint/i,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check the spoken script for retired offers

When loom-outline.md has a canonical ask but the generated or hand-edited recording-script.md still says “founder sprint” or “7-day sprint,” prospect:rehearsal can report the prospect ready: scoreProspect applies retiredOfferAsk only to the ask extracted from the outline, while the script's talkTrack is checked only for unsupported claims. Apply these retired-offer patterns to the talk track as well so the recording gate stops operators before they record stale spoken copy rather than relying on the later send-readiness check.

Useful? React with 👍 / 👎.

/tangible revenue (?:leak|fault) sprint/i,
/30[-\s]day action plan/i,
/growth desk/i,
/three pages/i,
/founder sprint/i,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check the sharpness brief for retired offers

When the outline contains a canonical ask but recording-sharpness-brief.md still says “founder sprint,” the rehearsal can report the prospect ready because scoreProspect reads brief but applies retiredOfferAsk only to the outline-derived ask. export-recording-teleprompter.mjs then renders that unchecked brief directly in the recording view, so the operator can still record from stale offer copy; include the brief in this retired-offer check.

Useful? React with 👍 / 👎.

/\$\s?500\b/i
];

Expand Down
2 changes: 1 addition & 1 deletion scripts/test-active-offer-projection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const testRoot = mkdtempSync(join(tmpdir(), "tinystudio-active-offer-projection-
const sp = name => join(repositoryRoot, "scripts", name)
const {equal: eq, match: mat, doesNotMatch: dnm} = assert
const legacyProspectPath = join(testRoot, "prospects", "legacy-offer-fixture")
const retiredOfferPattern = /7-Day Site Revenue Fault Sprint|Tangible Revenue Fault Sprint|30[- ]day action plan|Growth Desk|three pages|\$500/i
const retiredOfferPattern = /7-Day Site Revenue Fault Sprint|7[- ]day sprint|Tangible Revenue Fault Sprint|30[- ]day action plan|Growth Desk|three pages|founder sprint|\$500/i

function writeJson(path, value) {
mkdirSync(dirname(path), {recursive: true})
Expand Down
31 changes: 25 additions & 6 deletions scripts/test-outbound-send-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,38 @@ try {
assert(output(result).findings.some(finding => finding.rule === "send package still has placeholders"))

// The send package must never sell a retired broad-agency offer, and
// recording-notes.md is embedded verbatim into the send package.
for (const [file, content] of [
// recording-notes.md is embedded verbatim into the send package. Every
// active record/send surface must fail the gate when it still sells a
// retired offer (7-day sprint / 30-day action plan / founder sprint /
// $500 price), not just send-package.md and recording-notes.md.
const retiredFixtures = [
["send-package.md", `${packageHeader}7-Day Site Revenue Fault Sprint with a 30-day action plan. ${optOut}`],
["recording-notes.md", `# Recording Notes\n\n## Quality Notes\n\n- Clean ask: If useful, I can run a Tangible Revenue Fault Sprint with a 30-day action plan for $500.\n`]
]) {
["recording-notes.md", `# Recording Notes\n\n## Quality Notes\n\n- Clean ask: If useful, I can run a Tangible Revenue Fault Sprint with a 30-day action plan for $500.\n`],
["loom-outline.md", `# Loom Outline\n\n7. Sprint pitch: 7-Day Site Revenue Fault Sprint with a 30-day action plan\n`],
["recording-script.md", `# Recording Script\n\n## Talk Track\n\nIf useful, I can run a 7-day sprint where I map this fault for $1,000 founder sprint.\n`],
["next-message.md", `${packageHeader}If useful, I can run a 7-day sprint where I map this leak and give you a 30-day action plan. ${optOut}`],
["recording-sharpness-brief.md", `# Sharpness Brief\n\n## Positioning Angle\n\nOffer the The Website Correction at $1,000 founder sprint.\n`],
["outreach.md", `# Outreach\n\n## First Message\n\nIf useful, I can run a 7-day sprint where I map this fault and give you a 30-day action plan. ${optOut}`]
]
for (const [file, content] of retiredFixtures) {
writeFileSync(join(prospect, file), content)
result = run()
assert.notEqual(result.status, 0, `expected retired-offer failure for ${file}`)
assert(output(result).findings.some(finding => finding.rule === "outbound package sells a retired offer"))
Comment on lines +65 to 69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate each retired-surface fixture before running the gate

After the first iteration writes a retired send-package.md, that file remains in the fixture for every later iteration, and the assertion only checks that some retired-offer finding exists. Consequently, the test still passes if the checker completely ignores recording-notes.md, loom-outline.md, recording-script.md, and every other newly listed surface; remove or canonicalize the previous fixture before each run, or assert that the finding names the file under test.

Useful? React with 👍 / 👎.

}

writeFileSync(join(prospect, "send-package.md"), `${packageHeader}The Website Correction, one highest-leverage page, 14-day implementation tracking. ${optOut}`)
writeFileSync(join(prospect, "recording-notes.md"), `# Recording Notes\n\n## Quality Notes\n\n- Clean ask: If useful, I can run a human-reviewed The Website Correction on this one highest-leverage page with a measurement plan.\n`)
// Canonical copy on every surface must pass the retired-offer rule.
for (const [file, content] of [
["send-package.md", `${packageHeader}The Website Correction, one highest-leverage page, 14-day implementation tracking. ${optOut}`],
["recording-notes.md", `# Recording Notes\n\n## Quality Notes\n\n- Clean ask: If useful, I can run a human-reviewed The Website Correction on this one highest-leverage page with a measurement plan.\n`],
["loom-outline.md", `# Loom Outline\n\n7. Ask: If useful, I can run a human-reviewed The Website Correction on this one highest-leverage page with 14-day implementation tracking.\n`],
["recording-script.md", `# Recording Script\n\n## Talk Track\n\nIf useful, I can run a human-reviewed The Website Correction on this one highest-leverage page.\n`],
["next-message.md", `${packageHeader}If useful, I can run a human-reviewed The Website Correction on this one highest-leverage page. ${optOut}`],
["recording-sharpness-brief.md", `# Sharpness Brief\n\n## Positioning Angle\n\nThe Website Correction at the $1,000 founder pilot.\n`],
["outreach.md", `# Outreach\n\n## First Message\n\nIf useful, I can run a human-reviewed The Website Correction. ${optOut}`]
]) {
writeFileSync(join(prospect, file), content)
}
Comment on lines +56 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate and complete each outbound fixture case.

After the first iteration, send-package.md remains stale. Each later check can fail because of that file instead of the current fixture. The fixture list also omits reply-package.md, call-booked-package.md, close-package.md, audit-brief.md, buyer-room.md, and loom-package.md, although the readiness checker scans them.

Reset all outbound fixtures to canonical content before each retired case. Add a retired case for every filename in outboundFiles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-outbound-send-readiness.mjs` around lines 56 - 83, Update the
retired-fixture loop around retiredFixtures so every case starts with all
outbound files reset to canonical content, preventing prior iterations from
leaving send-package.md stale. Expand the retired cases to cover every filename
in outboundFiles, including reply-package.md, call-booked-package.md,
close-package.md, audit-brief.md, buyer-room.md, and loom-package.md, while
preserving the per-case failure assertion.

result = run()
assert.equal(result.status, 0, "canonical send package must not trigger the retired-offer rule")

Expand Down