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.
- package/dist/artifact-linter.js +574 -0
- package/dist/content/core-agents.js +21 -1
- package/dist/content/examples.js +9 -8
- package/dist/content/harness-doc.d.ts +1 -0
- package/dist/content/harness-doc.js +47 -0
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +369 -0
- package/dist/content/skills.d.ts +9 -0
- package/dist/content/skills.js +132 -5
- package/dist/content/stages/brainstorm.js +5 -5
- package/dist/content/status-command.js +8 -2
- package/dist/content/subagents.js +6 -2
- package/dist/content/templates.js +312 -20
- package/dist/content/tree-command.js +7 -1
- package/dist/delegation.d.ts +62 -4
- package/dist/delegation.js +218 -16
- package/dist/doctor-registry.js +9 -0
- package/dist/doctor.js +75 -2
- package/dist/harness-adapters.d.ts +48 -0
- package/dist/harness-adapters.js +123 -4
- package/dist/install.js +3 -1
- package/dist/internal/advance-stage.js +68 -18
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -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
|
|