cclaw-cli 0.51.25 → 0.51.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import { readConfig } from "./config.js";
6
6
  import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "./constants.js";
7
7
  import { exists } from "./fs-utils.js";
8
8
  import { stageSchema } from "./content/stage-schema.js";
9
+ import { CONFIDENCE_FINDING_REGEX_SOURCE, FORBIDDEN_PLACEHOLDER_TOKENS } from "./content/skills.js";
9
10
  import { FLOW_STAGES } from "./types.js";
10
11
  async function resolveNamedArtifactPath(projectRoot, fileName) {
11
12
  const relPath = path.join(RUNTIME_ROOT, "artifacts", fileName);
@@ -1922,6 +1923,147 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
1922
1923
  details: selfReview.details
1923
1924
  });
1924
1925
  }
1926
+ // Universal structural checks (Layer 2.1). Each fires only when the
1927
+ // matching section is present so legacy fixtures keep their current
1928
+ // shape, while artifacts emitted from the v3 template have to satisfy
1929
+ // them. Content is never inspected — only the shape required by the
1930
+ // reference patterns (gstack mode, forcing questions, premise list,
1931
+ // approach detail cards, anti-sycophancy stamp).
1932
+ const modeBody = sectionBodyByName(sections, "Mode Block");
1933
+ if (modeBody !== null) {
1934
+ const modeRegex = /\bMode:\s*(STARTUP|BUILDER|ENGINEERING|OPS|RESEARCH)\b/u;
1935
+ const ok = modeRegex.test(modeBody);
1936
+ findings.push({
1937
+ section: "Mode Block Token",
1938
+ required: true,
1939
+ rule: "Mode Block must declare exactly one mode token: STARTUP, BUILDER, ENGINEERING, OPS, or RESEARCH.",
1940
+ found: ok,
1941
+ details: ok
1942
+ ? "Recognized mode token detected."
1943
+ : "Mode Block is missing a recognized mode token (STARTUP/BUILDER/ENGINEERING/OPS/RESEARCH)."
1944
+ });
1945
+ }
1946
+ const forcingBody = sectionBodyByName(sections, "Forcing Questions");
1947
+ if (forcingBody !== null) {
1948
+ const tableRows = forcingBody
1949
+ .split("\n")
1950
+ .filter((line) => /^\|\s*\d+\s*\|/u.test(line));
1951
+ const enoughRows = tableRows.length >= 3;
1952
+ findings.push({
1953
+ section: "Forcing Questions Count",
1954
+ required: true,
1955
+ rule: "Forcing Questions must include at least 3 numbered rows.",
1956
+ found: enoughRows,
1957
+ details: enoughRows
1958
+ ? `Detected ${tableRows.length} forcing-question row(s).`
1959
+ : `Detected ${tableRows.length} forcing-question row(s); at least 3 required.`
1960
+ });
1961
+ // A "specific" answer is signalled by at least one of: numeric token,
1962
+ // backticked path/identifier, http(s) link, @mention/role, or quoted
1963
+ // verbatim string. We check structural shape, not content.
1964
+ const specificTokenRegex = /(\d|`[^`]+`|https?:\/\/|@[A-Za-z][\w-]*|"[^"]+"|'[^']+')/u;
1965
+ const allRowsSpecific = tableRows.every((row) => {
1966
+ const cells = row.split("|").map((cell) => cell.trim());
1967
+ // cells: ["", "#", "Question", "Answer", "Decision impact", "Q<n> decision", ""]
1968
+ const answer = cells[3] ?? "";
1969
+ return answer.length > 0 && specificTokenRegex.test(answer);
1970
+ });
1971
+ findings.push({
1972
+ section: "Forcing Questions Specific Answers",
1973
+ required: true,
1974
+ rule: "Each Forcing Questions row must include a specific token in the answer column (number, backticked path, link, @mention, or quoted string).",
1975
+ found: tableRows.length === 0 ? false : allRowsSpecific,
1976
+ details: tableRows.length === 0
1977
+ ? "No rows to evaluate."
1978
+ : allRowsSpecific
1979
+ ? "All rows include a specific-answer token."
1980
+ : "At least one row's answer is missing a specific-answer token (number, `path`, https link, @mention, or quoted string)."
1981
+ });
1982
+ const decisionRows = (forcingBody.match(/decision\s*:/giu) ?? []).length;
1983
+ findings.push({
1984
+ section: "Forcing Questions STOP-per-issue",
1985
+ required: true,
1986
+ rule: "Each forcing-question row must record a `decision:` marker (STOP-per-issue protocol).",
1987
+ found: decisionRows >= tableRows.length && tableRows.length > 0,
1988
+ details: tableRows.length === 0
1989
+ ? "No rows to evaluate."
1990
+ : `Detected ${decisionRows} decision marker(s) for ${tableRows.length} forcing-question row(s).`
1991
+ });
1992
+ }
1993
+ const premiseBody = sectionBodyByName(sections, "Premise List");
1994
+ if (premiseBody !== null) {
1995
+ const premiseRowRegex = /^[-*]\s*P\d+:\s+.+\s+—\s+(agreed|disagreed|revised)\b/imu;
1996
+ const allRows = premiseBody
1997
+ .split("\n")
1998
+ .filter((line) => /^[-*]\s*P\d+:/u.test(line));
1999
+ const validRows = allRows.filter((row) => premiseRowRegex.test(row.trim() + "\n"));
2000
+ const enoughPremises = validRows.length >= 2;
2001
+ findings.push({
2002
+ section: "Premise List Shape",
2003
+ required: true,
2004
+ rule: "Premise List must contain at least 2 rows in the form `P<n>: <statement> — agreed|disagreed|revised`.",
2005
+ found: enoughPremises,
2006
+ details: enoughPremises
2007
+ ? `Detected ${validRows.length} valid premise row(s).`
2008
+ : `Detected ${validRows.length} valid premise row(s); at least 2 required (form: \`P<n>: ... — agreed|disagreed|revised\`).`
2009
+ });
2010
+ }
2011
+ // Approach Detail Cards: structural sub-section under Approaches, one
2012
+ // bullet block per approach with the canonical fields.
2013
+ const approachCardsRegex = /####\s+APPROACH\s+[A-Z]\b[\s\S]*?(?:^-\s*Summary:[\s\S]*?^-\s*Effort:[\s\S]*?^-\s*Risk:[\s\S]*?^-\s*Pros:[\s\S]*?^-\s*Cons:[\s\S]*?^-\s*Reuses:)/gimu;
2014
+ const matches = raw.match(approachCardsRegex);
2015
+ const cardCount = matches ? matches.length : 0;
2016
+ if (/####\s+APPROACH\s+[A-Z]\b/iu.test(raw) ||
2017
+ /^RECOMMENDATION:/imu.test(raw)) {
2018
+ findings.push({
2019
+ section: "Approach Detail Cards",
2020
+ required: true,
2021
+ rule: "Approach Detail Cards must include ≥2 `#### APPROACH <letter>` blocks each with Summary/Effort/Risk/Pros/Cons/Reuses.",
2022
+ found: cardCount >= 2,
2023
+ details: cardCount >= 2
2024
+ ? `Detected ${cardCount} valid approach detail card(s).`
2025
+ : `Detected ${cardCount} valid approach detail card(s); at least 2 required with all fields present.`
2026
+ });
2027
+ const recommendationLine = raw.match(/^RECOMMENDATION:\s*(.+)$/imu);
2028
+ const hasRecommendation = recommendationLine !== null && recommendationLine[1] !== undefined && recommendationLine[1].trim().length > 0;
2029
+ findings.push({
2030
+ section: "Approach Recommendation Marker",
2031
+ required: true,
2032
+ rule: "Approach Detail Cards must conclude with a single `RECOMMENDATION:` line citing the chosen letter and rationale.",
2033
+ found: hasRecommendation,
2034
+ details: hasRecommendation
2035
+ ? "Recommendation marker present."
2036
+ : "Missing or empty `RECOMMENDATION:` line after approach detail cards."
2037
+ });
2038
+ }
2039
+ const stampBody = sectionBodyByName(sections, "Anti-Sycophancy Stamp");
2040
+ if (stampBody !== null) {
2041
+ const acknowledged = /forbidden response openers acknowledged:\s*(yes|true|y)\b/iu.test(stampBody);
2042
+ findings.push({
2043
+ section: "Anti-Sycophancy Acknowledgement",
2044
+ required: true,
2045
+ rule: "Anti-Sycophancy Stamp must affirm `Forbidden response openers acknowledged: yes`.",
2046
+ found: acknowledged,
2047
+ details: acknowledged
2048
+ ? "Anti-sycophancy commitment is acknowledged."
2049
+ : "Anti-Sycophancy Stamp is missing the explicit `Forbidden response openers acknowledged: yes` marker."
2050
+ });
2051
+ }
2052
+ const outsideVoiceBody = sectionBodyByName(sections, "Outside Voice");
2053
+ if (outsideVoiceBody !== null) {
2054
+ const required = ["source:", "prompt:", "tension:", "resolution:"];
2055
+ const missing = required.filter((key) => !new RegExp(`(?:^|\\n)\\s*-?\\s*${key.replace(":", "\\s*:")}`, "iu").test(outsideVoiceBody));
2056
+ const optedOut = /\bnot used\b|\bn\/a\b|\bnone\b/iu.test(outsideVoiceBody);
2057
+ findings.push({
2058
+ section: "Outside Voice Slot Shape",
2059
+ required: true,
2060
+ rule: "Outside Voice section must either declare opt-out (`not used`/`none`) or include `source:`, `prompt:`, `tension:`, `resolution:`.",
2061
+ found: optedOut || missing.length === 0,
2062
+ details: optedOut || missing.length === 0
2063
+ ? "Outside Voice slot is well-formed."
2064
+ : `Outside Voice section is missing field(s): ${missing.join(", ")}.`
2065
+ });
2066
+ }
1925
2067
  }
1926
2068
  if (stage === "design") {
1927
2069
  const tierResolution = await resolveDesignDiagramTier(projectRoot, track, raw);
@@ -1975,6 +2117,58 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
1975
2117
  });
1976
2118
  }
1977
2119
  }
2120
+ // Universal Layer 2.3 structural checks (gstack plan-eng-review). All
2121
+ // present-only. Validates ASCII coverage diagram tokens, regression iron
2122
+ // rule acknowledgment, and confidence-calibrated finding format.
2123
+ const coverageBody = sectionBodyByName(sections, "ASCII Coverage Diagram");
2124
+ if (coverageBody !== null) {
2125
+ const tokens = ["[★★★]", "[★★]", "[★]", "[GAP]", "[→E2E]", "[→EVAL]"];
2126
+ const presentTokens = tokens.filter((token) => coverageBody.includes(token));
2127
+ const ok = presentTokens.length >= 3;
2128
+ findings.push({
2129
+ section: "ASCII Coverage Diagram Tokens",
2130
+ required: true,
2131
+ rule: "ASCII Coverage Diagram must use the canonical marker tokens (at least 3 of `[★★★]` / `[★★]` / `[★]` / `[GAP]` / `[→E2E]` / `[→EVAL]`).",
2132
+ found: ok,
2133
+ details: ok
2134
+ ? `Detected ${presentTokens.length} canonical marker token(s).`
2135
+ : `Detected ${presentTokens.length} canonical marker token(s); at least 3 required.`
2136
+ });
2137
+ }
2138
+ const regressionBody = sectionBodyByName(sections, "Regression Iron Rule");
2139
+ if (regressionBody !== null) {
2140
+ const ack = /iron rule acknowledged:\s*(yes|true|y)\b/iu.test(regressionBody);
2141
+ findings.push({
2142
+ section: "Regression Iron Rule Acknowledgement",
2143
+ required: true,
2144
+ rule: "Regression Iron Rule section must affirm `Iron rule acknowledged: yes`.",
2145
+ found: ack,
2146
+ details: ack
2147
+ ? "Regression iron rule acknowledged."
2148
+ : "Regression Iron Rule is missing explicit `Iron rule acknowledged: yes`."
2149
+ });
2150
+ }
2151
+ const findingsBody = sectionBodyByName(sections, "Calibrated Findings");
2152
+ if (findingsBody !== null) {
2153
+ const isEmpty = /(^|\n)\s*-\s*None this stage\b/iu.test(findingsBody);
2154
+ const findingRegex = new RegExp(CONFIDENCE_FINDING_REGEX_SOURCE, "u");
2155
+ const validRows = findingsBody
2156
+ .split("\n")
2157
+ .filter((line) => /^[-*]\s+\[/u.test(line.trim()))
2158
+ .filter((line) => findingRegex.test(line));
2159
+ const ok = isEmpty || validRows.length >= 1;
2160
+ findings.push({
2161
+ section: "Calibrated Finding Format",
2162
+ required: true,
2163
+ rule: "Calibrated Findings must either declare `None this stage` or contain at least one finding in the form `[P1|P2|P3] (confidence: <n>/10) <path>[:<line>] — <description>`.",
2164
+ found: ok,
2165
+ details: isEmpty
2166
+ ? "No findings recorded for this stage."
2167
+ : ok
2168
+ ? `Detected ${validRows.length} calibrated finding(s).`
2169
+ : "No calibrated findings detected. Use `[P1|P2|P3] (confidence: <n>/10) <repo-path>[:<line>] — <description>`."
2170
+ });
2171
+ }
1978
2172
  }
1979
2173
  if (stage === "plan") {
1980
2174
  const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
@@ -2025,6 +2219,79 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
2025
2219
  ? "No scope-reduction phrases detected in Task List."
2026
2220
  : `Detected scope-reduction phrase(s) in Task List: ${reductionHits.join(", ")}.`
2027
2221
  });
2222
+ // Universal Layer 2.5 structural checks (superpowers writing-plans + ce-plan).
2223
+ // Plan-wide placeholder scan (broader than Task List) using the
2224
+ // FORBIDDEN_PLACEHOLDER_TOKENS list shared with the cross-cutting block.
2225
+ const planHeaderBody = sectionBodyByName(sections, "Plan Header");
2226
+ if (planHeaderBody !== null) {
2227
+ const required = ["Goal:", "Architecture:", "Tech Stack:"];
2228
+ const missing = required.filter((token) => !new RegExp(token.replace(":", "\\s*:"), "iu").test(planHeaderBody));
2229
+ findings.push({
2230
+ section: "Plan Header Coverage",
2231
+ required: true,
2232
+ rule: "Plan Header must include Goal, Architecture, and Tech Stack lines.",
2233
+ found: missing.length === 0,
2234
+ details: missing.length === 0
2235
+ ? "Plan Header covers Goal/Architecture/Tech Stack."
2236
+ : `Plan Header is missing field(s): ${missing.join(", ")}.`
2237
+ });
2238
+ }
2239
+ const unitBlocks = raw.match(/###\s+Implementation Unit\s+U-\d+/giu) ?? [];
2240
+ if (unitBlocks.length > 0) {
2241
+ const requiredKeys = ["Goal:", "Files", "Approach:", "Test scenarios:", "Verification:"];
2242
+ const blockBodies = raw.split(/(?=###\s+Implementation Unit\s+U-\d+)/iu).slice(1);
2243
+ const validBlocks = blockBodies.filter((block) => requiredKeys.every((key) => new RegExp(key.replace(":", "\\s*:"), "iu").test(block)));
2244
+ findings.push({
2245
+ section: "Implementation Unit Shape",
2246
+ required: true,
2247
+ rule: "Each `### Implementation Unit U-<n>` must include Goal, Files, Approach, Test scenarios, Verification.",
2248
+ found: validBlocks.length === unitBlocks.length,
2249
+ details: validBlocks.length === unitBlocks.length
2250
+ ? `All ${unitBlocks.length} implementation unit(s) include the required fields.`
2251
+ : `${unitBlocks.length - validBlocks.length} implementation unit(s) are missing required fields.`
2252
+ });
2253
+ }
2254
+ const allPlaceholderTokens = FORBIDDEN_PLACEHOLDER_TOKENS.map((token) => token.toLowerCase());
2255
+ const lowerRaw = raw.toLowerCase();
2256
+ const planWidePlaceholderHits = allPlaceholderTokens.filter((token) => lowerRaw.includes(token));
2257
+ // Strip the "## NO PLACEHOLDERS Rule" section (which lists tokens) and
2258
+ // any acknowledgement text from the scan to avoid false positives where
2259
+ // the plan deliberately references the rule by name.
2260
+ const placeholderRuleSection = sectionBodyByName(sections, "NO PLACEHOLDERS Rule");
2261
+ const ruleScanBody = (placeholderRuleSection ?? "").toLowerCase();
2262
+ const ruleAcceptedHits = ruleScanBody.length > 0
2263
+ ? allPlaceholderTokens.filter((token) => ruleScanBody.includes(token))
2264
+ : [];
2265
+ const filteredPlanHits = planWidePlaceholderHits.filter((token) => {
2266
+ // If the only occurrence is in the rule section, ignore it.
2267
+ if (!ruleAcceptedHits.includes(token))
2268
+ return true;
2269
+ const occurrencesElsewhere = lowerRaw.split(token).length - 1
2270
+ - (ruleScanBody.split(token).length - 1);
2271
+ return occurrencesElsewhere > 0;
2272
+ });
2273
+ findings.push({
2274
+ section: "Plan-wide Placeholder Scan",
2275
+ required: false,
2276
+ rule: "Plan should not contain forbidden placeholder tokens outside the NO PLACEHOLDERS rule section.",
2277
+ found: filteredPlanHits.length === 0,
2278
+ details: filteredPlanHits.length === 0
2279
+ ? "No forbidden placeholder tokens detected outside the rule section."
2280
+ : `Detected forbidden token(s) elsewhere in plan: ${filteredPlanHits.join(", ")}.`
2281
+ });
2282
+ const handoffBody = sectionBodyByName(sections, "Execution Handoff");
2283
+ if (handoffBody !== null) {
2284
+ const ok = /(subagent-driven|inline executor)/iu.test(handoffBody);
2285
+ findings.push({
2286
+ section: "Execution Handoff Posture",
2287
+ required: true,
2288
+ rule: "Execution Handoff must declare a posture (Subagent-Driven or Inline executor).",
2289
+ found: ok,
2290
+ details: ok
2291
+ ? "Execution Handoff posture declared."
2292
+ : "Execution Handoff is missing a posture declaration (Subagent-Driven or Inline executor)."
2293
+ });
2294
+ }
2028
2295
  }
2029
2296
  if (stage === "scope") {
2030
2297
  const lockedDecisionsBody = sectionBodyByHeadingPrefix(sections, "Locked Decisions") ?? "";
@@ -2099,6 +2366,313 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
2099
2366
  : issues.join("; ")
2100
2367
  });
2101
2368
  }
2369
+ // Universal Layer 2.2 structural checks (gstack plan-ceo-review). All
2370
+ // present-only — they validate shape when the section exists.
2371
+ const altsBody = sectionBodyByName(sections, "Implementation Alternatives");
2372
+ if (altsBody !== null) {
2373
+ const recommendation = /^RECOMMENDATION:\s*(.+)$/imu.test(altsBody);
2374
+ findings.push({
2375
+ section: "Implementation Alternatives Recommendation",
2376
+ required: true,
2377
+ rule: "Implementation Alternatives must conclude with a `RECOMMENDATION:` line citing the chosen option and rationale.",
2378
+ found: recommendation,
2379
+ details: recommendation
2380
+ ? "Recommendation marker present."
2381
+ : "Missing or empty `RECOMMENDATION:` line under Implementation Alternatives."
2382
+ });
2383
+ }
2384
+ const failureModesBody = sectionBodyByName(sections, "Failure Modes Registry");
2385
+ if (failureModesBody !== null) {
2386
+ const required = ["Codepath", "Failure mode", "Rescued?", "Test?", "User sees?", "Logged?"];
2387
+ const headerOk = required.every((column) => failureModesBody.includes(column));
2388
+ const rows = failureModesBody.split("\n").filter((line) => /^\|/u.test(line));
2389
+ const hasDataRow = rows.length >= 3 && rows.slice(2).some((row) => row
2390
+ .split("|")
2391
+ .slice(1, -1)
2392
+ .some((cell) => cell.trim().length > 0));
2393
+ findings.push({
2394
+ section: "Failure Modes Registry Shape",
2395
+ required: true,
2396
+ rule: "Failure Modes Registry must include columns Codepath / Failure mode / Rescued? / Test? / User sees? / Logged? and at least one populated data row.",
2397
+ found: headerOk && hasDataRow,
2398
+ details: !headerOk
2399
+ ? "Failure Modes Registry header is missing one or more required columns."
2400
+ : hasDataRow
2401
+ ? "Failure Modes Registry header and at least one data row present."
2402
+ : "Failure Modes Registry has the canonical header but no populated data row."
2403
+ });
2404
+ const decisionMarkers = (failureModesBody.match(/decision\s*:/giu) ?? []).length;
2405
+ findings.push({
2406
+ section: "Failure Modes STOP-per-issue",
2407
+ required: true,
2408
+ rule: "Each Failure Modes Registry data row must record a `decision:` marker (STOP-per-issue protocol).",
2409
+ found: decisionMarkers >= 1,
2410
+ details: decisionMarkers >= 1
2411
+ ? `Detected ${decisionMarkers} decision marker(s).`
2412
+ : "Failure Modes Registry has no `decision:` markers; STOP-per-issue requires at least one."
2413
+ });
2414
+ }
2415
+ const reversibilityBody = sectionBodyByName(sections, "Reversibility Rating");
2416
+ if (reversibilityBody !== null) {
2417
+ const scoreMatch = reversibilityBody.match(/score\s*\(.*?\)\s*:\s*([1-5])\b/iu);
2418
+ const ok = scoreMatch !== null;
2419
+ findings.push({
2420
+ section: "Reversibility Rating Score",
2421
+ required: true,
2422
+ rule: "Reversibility Rating must declare a score in the range 1-5.",
2423
+ found: ok,
2424
+ details: ok
2425
+ ? `Reversibility score ${scoreMatch[1]} declared.`
2426
+ : "Reversibility Rating is missing a numeric score 1-5."
2427
+ });
2428
+ }
2429
+ }
2430
+ if (stage === "spec") {
2431
+ // Universal Layer 2.4 structural checks (evanflow-prd + superpowers).
2432
+ // All checks fire only when the matching section is present so legacy
2433
+ // fixtures keep working while v3-template artifacts are validated.
2434
+ const synthesisBody = sectionBodyByName(sections, "Synthesis Sources");
2435
+ if (synthesisBody !== null) {
2436
+ const tableRows = synthesisBody
2437
+ .split("\n")
2438
+ .filter((line) => /^\|/u.test(line));
2439
+ const dataRows = tableRows.length >= 3 ? tableRows.slice(2) : [];
2440
+ const populatedRows = dataRows.filter((row) => row
2441
+ .split("|")
2442
+ .slice(1, -1)
2443
+ .some((cell) => cell.trim().length > 0));
2444
+ const hasRow = populatedRows.length >= 1;
2445
+ findings.push({
2446
+ section: "Synthesis Sources Coverage",
2447
+ required: true,
2448
+ rule: "Synthesis Sources must cite at least one source artifact (synthesize-not-interview).",
2449
+ found: hasRow,
2450
+ details: hasRow
2451
+ ? `Detected ${populatedRows.length} populated source row(s).`
2452
+ : "Synthesis Sources is empty; spec must cite at least one upstream artifact or context file."
2453
+ });
2454
+ }
2455
+ const behaviorBody = sectionBodyByName(sections, "Behavior Contract");
2456
+ if (behaviorBody !== null) {
2457
+ const optedOut = /(^|\n)\s*-\s*None\b/iu.test(behaviorBody);
2458
+ const userStoryRegex = /(^|\n)\s*-\s*as\s+a\b[\s\S]*?,\s*i\s+can\b[\s\S]*?,\s*so that\b/imu;
2459
+ const givenWhenThenRegex = /(^|\n)\s*-\s*given\b[\s\S]*?,\s*when\b[\s\S]*?,\s*then\b/imu;
2460
+ const matches = [
2461
+ ...behaviorBody.matchAll(/(^|\n)\s*-\s*as\s+a\b[\s\S]*?,\s*i\s+can\b[\s\S]*?,\s*so that\b/gimu),
2462
+ ...behaviorBody.matchAll(/(^|\n)\s*-\s*given\b[\s\S]*?,\s*when\b[\s\S]*?,\s*then\b/gimu)
2463
+ ];
2464
+ const ok = optedOut || matches.length >= 3;
2465
+ findings.push({
2466
+ section: "Behavior Contract Shape",
2467
+ required: true,
2468
+ rule: "Behavior Contract must list ≥3 behaviors in user-story (As a/I can/so that) or Given/When/Then form, or declare `- None.` for single-step specs.",
2469
+ found: ok,
2470
+ details: optedOut
2471
+ ? "Single-step spec; behaviors opted out via `- None.`."
2472
+ : ok
2473
+ ? `Detected ${matches.length} behavior(s) in canonical form.`
2474
+ : `Detected ${matches.length} behavior(s) in canonical form; need ≥3 (or `
2475
+ + "`- None.`).",
2476
+ });
2477
+ // Bonus: detect if at least one user-story OR given/when/then form is present
2478
+ // (mirrors existing helpers).
2479
+ void userStoryRegex;
2480
+ void givenWhenThenRegex;
2481
+ }
2482
+ const archModulesBody = sectionBodyByName(sections, "Architecture Modules");
2483
+ if (archModulesBody !== null) {
2484
+ const codeFenceCount = (archModulesBody.match(/```/gu) ?? []).length;
2485
+ const fnSignatureRegex = /\b(function|class|def|fn|method)\b\s+[A-Za-z_]/u;
2486
+ const noCode = codeFenceCount === 0 && !fnSignatureRegex.test(archModulesBody);
2487
+ findings.push({
2488
+ section: "Architecture Modules No-Code",
2489
+ required: true,
2490
+ rule: "Architecture Modules must not contain code blocks, function signatures, or class definitions — modules listed by responsibility only.",
2491
+ found: noCode,
2492
+ details: noCode
2493
+ ? "Architecture Modules is free of code blocks and function/class signatures."
2494
+ : "Architecture Modules contains a code fence or function/class signature; remove code-level details."
2495
+ });
2496
+ }
2497
+ const selfReviewBody = sectionBodyByName(sections, "Spec Self-Review");
2498
+ if (selfReviewBody !== null) {
2499
+ const required = ["placeholder", "consistency", "scope", "ambiguity"];
2500
+ const missing = required.filter((token) => !new RegExp(token, "iu").test(selfReviewBody));
2501
+ findings.push({
2502
+ section: "Spec Self-Review Coverage",
2503
+ required: true,
2504
+ rule: "Spec Self-Review must cover placeholder/consistency/scope/ambiguity checks.",
2505
+ found: missing.length === 0,
2506
+ details: missing.length === 0
2507
+ ? "Spec Self-Review covers all required checks."
2508
+ : `Spec Self-Review is missing check(s): ${missing.join(", ")}.`
2509
+ });
2510
+ }
2511
+ }
2512
+ if (stage === "tdd") {
2513
+ // Universal Layer 2.6 structural checks (superpowers TDD + evanflow vertical slices).
2514
+ const ironLawBody = sectionBodyByName(sections, "Iron Law Acknowledgement");
2515
+ if (ironLawBody !== null) {
2516
+ const ack = /acknowledged:\s*(yes|true|y)\b/iu.test(ironLawBody);
2517
+ findings.push({
2518
+ section: "TDD Iron Law Acknowledgement",
2519
+ required: true,
2520
+ rule: "Iron Law Acknowledgement must affirm `Acknowledged: yes`.",
2521
+ found: ack,
2522
+ details: ack
2523
+ ? "TDD Iron Law acknowledged."
2524
+ : "Iron Law Acknowledgement is missing explicit `Acknowledged: yes`."
2525
+ });
2526
+ }
2527
+ const watchedRedBody = sectionBodyByName(sections, "Watched-RED Proof");
2528
+ if (watchedRedBody !== null) {
2529
+ const rows = watchedRedBody.split("\n").filter((line) => /^\|/u.test(line));
2530
+ const dataRows = rows.length >= 3 ? rows.slice(2) : [];
2531
+ const populatedRows = dataRows.filter((row) => row
2532
+ .split("|")
2533
+ .slice(1, -1)
2534
+ .filter((_, idx) => idx !== 0) // skip slice column
2535
+ .some((cell) => cell.trim().length > 0));
2536
+ // Each populated row must include an ISO timestamp in column 3.
2537
+ const isoRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/u;
2538
+ const validProofRows = populatedRows.filter((row) => isoRegex.test(row));
2539
+ findings.push({
2540
+ section: "Watched-RED Proof Shape",
2541
+ required: true,
2542
+ rule: "Watched-RED Proof rows must include an ISO timestamp showing when the test was observed failing.",
2543
+ found: populatedRows.length === 0 || validProofRows.length === populatedRows.length,
2544
+ details: populatedRows.length === 0
2545
+ ? "Watched-RED Proof has no populated rows."
2546
+ : validProofRows.length === populatedRows.length
2547
+ ? `All ${populatedRows.length} watched-RED proof row(s) include an ISO timestamp.`
2548
+ : `${populatedRows.length - validProofRows.length} watched-RED proof row(s) lack an ISO timestamp.`
2549
+ });
2550
+ }
2551
+ const sliceCycleBody = sectionBodyByName(sections, "Vertical Slice Cycle");
2552
+ if (sliceCycleBody !== null) {
2553
+ const required = ["RED", "GREEN", "REFACTOR"];
2554
+ const missing = required.filter((token) => !new RegExp(token, "u").test(sliceCycleBody));
2555
+ findings.push({
2556
+ section: "Vertical Slice Cycle Coverage",
2557
+ required: true,
2558
+ rule: "Vertical Slice Cycle must include RED, GREEN, and REFACTOR per slice (refactor may be deferred with rationale).",
2559
+ found: missing.length === 0,
2560
+ details: missing.length === 0
2561
+ ? "Vertical Slice Cycle references RED/GREEN/REFACTOR."
2562
+ : `Vertical Slice Cycle is missing phase token(s): ${missing.join(", ")}.`
2563
+ });
2564
+ }
2565
+ const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
2566
+ if (assertionBody !== null) {
2567
+ const tableRows = assertionBody.split("\n").filter((line) => /^\|/u.test(line));
2568
+ const dataRows = tableRows.length >= 3 ? tableRows.slice(2) : [];
2569
+ const ok = dataRows.length === 0 || dataRows.some((row) => row
2570
+ .split("|")
2571
+ .slice(1, -1)
2572
+ .some((cell) => cell.trim().length > 0));
2573
+ findings.push({
2574
+ section: "Assertion Correctness Notes Shape",
2575
+ required: true,
2576
+ rule: "Assertion Correctness Notes must include at least one populated row when the slice has new assertions.",
2577
+ found: ok,
2578
+ details: ok
2579
+ ? "Assertion Correctness Notes is populated or absent (single-step slice)."
2580
+ : "Assertion Correctness Notes table has no populated rows."
2581
+ });
2582
+ }
2583
+ }
2584
+ if (stage === "review") {
2585
+ // Universal Layer 2.7 structural checks (superpowers requesting + receiving).
2586
+ const frameBody = sectionBodyByName(sections, "Frame the Review Request");
2587
+ if (frameBody !== null) {
2588
+ const required = ["Goal:", "Approach:", "Risk areas:", "Verification done:", "Open questions"];
2589
+ const missing = required.filter((token) => !new RegExp(token.replace(":", "\\s*:"), "iu").test(frameBody));
2590
+ findings.push({
2591
+ section: "Review Frame Coverage",
2592
+ required: true,
2593
+ rule: "Frame the Review Request must include Goal, Approach, Risk areas, Verification done, Open questions.",
2594
+ found: missing.length === 0,
2595
+ details: missing.length === 0
2596
+ ? "Review request frame covers all required fields."
2597
+ : `Frame is missing field(s): ${missing.join(", ")}.`
2598
+ });
2599
+ }
2600
+ const criticBody = sectionBodyByName(sections, "Critic Subagent Dispatch");
2601
+ if (criticBody !== null) {
2602
+ const required = [
2603
+ "Critic agent definition path",
2604
+ "Dispatch surface",
2605
+ "Frame sent",
2606
+ "Critic returned"
2607
+ ];
2608
+ const missing = required.filter((token) => !criticBody.includes(token));
2609
+ findings.push({
2610
+ section: "Critic Subagent Dispatch Shape",
2611
+ required: true,
2612
+ rule: "Critic Subagent Dispatch must declare agent definition path, dispatch surface, frame sent, and critic-returned summary.",
2613
+ found: missing.length === 0,
2614
+ details: missing.length === 0
2615
+ ? "Critic dispatch metadata complete."
2616
+ : `Critic Subagent Dispatch is missing field(s): ${missing.join(", ")}.`
2617
+ });
2618
+ }
2619
+ const receivingBody = sectionBodyByName(sections, "Receiving Posture");
2620
+ if (receivingBody !== null) {
2621
+ const ack = /no performative agreement/iu.test(receivingBody);
2622
+ findings.push({
2623
+ section: "Receiving Posture Anti-Sycophancy",
2624
+ required: true,
2625
+ rule: "Receiving Posture must affirm `No performative agreement (forbidden openers acknowledged)`.",
2626
+ found: ack,
2627
+ details: ack
2628
+ ? "Receiving posture acknowledged anti-sycophancy."
2629
+ : "Receiving Posture is missing the anti-sycophancy acknowledgement line."
2630
+ });
2631
+ }
2632
+ }
2633
+ if (stage === "ship") {
2634
+ // Universal Layer 2.8 structural checks (superpowers finishing-a-development-branch).
2635
+ const optionsBody = sectionBodyByName(sections, "Finalization Options");
2636
+ if (optionsBody !== null) {
2637
+ const required = ["MERGE_LOCAL", "OPEN_PR", "KEEP_BRANCH", "DISCARD"];
2638
+ const missing = required.filter((token) => !optionsBody.includes(token));
2639
+ findings.push({
2640
+ section: "Finalization Options Coverage",
2641
+ required: true,
2642
+ rule: "Finalization Options must surface all four canonical options (MERGE_LOCAL, OPEN_PR, KEEP_BRANCH, DISCARD).",
2643
+ found: missing.length === 0,
2644
+ details: missing.length === 0
2645
+ ? "All four finalization options surfaced."
2646
+ : `Finalization Options is missing token(s): ${missing.join(", ")}.`
2647
+ });
2648
+ }
2649
+ const prBody = sectionBodyByName(sections, "Structured PR Body");
2650
+ if (prBody !== null) {
2651
+ const required = ["## Summary", "## Test Plan", "## Commits Included"];
2652
+ const missing = required.filter((token) => !prBody.includes(token));
2653
+ findings.push({
2654
+ section: "Structured PR Body Shape",
2655
+ required: true,
2656
+ rule: "Structured PR Body must include `## Summary`, `## Test Plan`, and `## Commits Included` subsections.",
2657
+ found: missing.length === 0,
2658
+ details: missing.length === 0
2659
+ ? "Structured PR Body covers all required subsections."
2660
+ : `Structured PR Body is missing subsection(s): ${missing.join(", ")}.`
2661
+ });
2662
+ }
2663
+ const verifyBody = sectionBodyByName(sections, "Verify Tests Gate");
2664
+ if (verifyBody !== null) {
2665
+ const ok = /\bResult:\s*(PASS|FAIL)\b/iu.test(verifyBody);
2666
+ findings.push({
2667
+ section: "Verify Tests Gate Result",
2668
+ required: true,
2669
+ rule: "Verify Tests Gate must declare a Result of PASS or FAIL.",
2670
+ found: ok,
2671
+ details: ok
2672
+ ? "Verify Tests Gate result declared."
2673
+ : "Verify Tests Gate is missing a `Result: PASS|FAIL` line."
2674
+ });
2675
+ }
2102
2676
  }
2103
2677
  if (["design", "spec", "plan", "review"].includes(stage)) {
2104
2678
  const scopeArtifact = await resolveStageArtifactPath("scope", {
@@ -30,6 +30,24 @@ const DOC_RETURN_SCHEMA = {
30
30
  requiredFields: ["status", "filesUpdated", "summary", "evidenceRefs", "openQuestions"],
31
31
  evidenceFields: ["filesUpdated", "evidenceRefs"]
32
32
  };
33
+ function workerAckContract() {
34
+ return `## Worker ACK Contract
35
+
36
+ Before doing substantive work, return an ACK object that the parent can record:
37
+
38
+ \`\`\`json
39
+ {
40
+ "status": "ACK",
41
+ "spanId": "<parent spanId>",
42
+ "dispatchId": "<parent dispatchId or workerRunId>",
43
+ "dispatchSurface": "claude-task|cursor-task|opencode-agent|codex-agent|generic-task|role-switch",
44
+ "agentDefinitionPath": ".cclaw/agents/<agent>.md or harness generated path",
45
+ "ackTs": "<ISO timestamp>"
46
+ }
47
+ \`\`\`
48
+
49
+ Finish with the required return schema plus the same \`spanId\` and \`dispatchId\`. The parent must not claim isolated completion unless ACK/result proof matches the ledger/event span.`;
50
+ }
33
51
  function formatReturnSchema(schema) {
34
52
  return [
35
53
  `- Status field: \`${schema.statusField}\``,
@@ -446,9 +464,11 @@ ${agent.body}
446
464
  - Mode: ${agent.activation}
447
465
  - Related stages: ${relatedStages}
448
466
 
467
+ ${workerAckContract()}
468
+
449
469
  ## Required Return Schema
450
470
 
451
- STRICT_RETURN_SCHEMA: return a structured object matching this contract before any narrative when delegated.
471
+ STRICT_RETURN_SCHEMA: return a structured object matching this contract before any narrative when delegated. Include \`spanId\`, \`dispatchId\` or \`workerRunId\`, \`dispatchSurface\`, \`agentDefinitionPath\`, and lifecycle timestamps when provided by the parent.
452
472
 
453
473
  ${formatReturnSchema(agent.returnSchema)}
454
474