cclaw-cli 0.51.23 → 0.51.25
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/README.md +135 -414
- package/dist/artifact-linter.js +10 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +28 -3
- package/dist/content/core-agents.d.ts +128 -2
- package/dist/content/core-agents.js +291 -13
- package/dist/content/examples.js +21 -10
- package/dist/content/next-command.js +10 -6
- package/dist/content/reference-patterns.d.ts +18 -0
- package/dist/content/reference-patterns.js +391 -0
- package/dist/content/seed-shelf.js +73 -8
- package/dist/content/skills.js +39 -34
- package/dist/content/stage-common-guidance.js +19 -3
- package/dist/content/stage-schema.d.ts +12 -0
- package/dist/content/stage-schema.js +224 -24
- package/dist/content/stages/_lint-metadata/index.js +3 -2
- package/dist/content/stages/brainstorm.js +27 -18
- package/dist/content/stages/design.js +27 -18
- package/dist/content/stages/review.js +20 -9
- package/dist/content/stages/schema-types.d.ts +9 -2
- package/dist/content/stages/scope.js +21 -10
- package/dist/content/stages/ship.js +3 -2
- package/dist/content/stages/tdd.js +18 -13
- package/dist/content/start-command.js +3 -2
- package/dist/content/status-command.js +9 -4
- package/dist/content/subagents.js +336 -38
- package/dist/content/templates.js +182 -25
- package/dist/delegation.d.ts +2 -0
- package/dist/delegation.js +27 -6
- package/dist/doctor.js +167 -25
- package/dist/flow-state.d.ts +1 -0
- package/dist/flow-state.js +1 -0
- package/dist/gate-evidence.js +25 -2
- package/dist/install.js +72 -8
- package/dist/internal/advance-stage.js +179 -26
- package/dist/knowledge-store.js +30 -6
- package/dist/run-archive.js +11 -0
- package/dist/run-persistence.js +35 -10
- package/dist/tdd-verification-evidence.d.ts +17 -0
- package/dist/tdd-verification-evidence.js +43 -0
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -24,6 +24,7 @@ import { resolveTrackFromPrompt } from "./track-heuristics.js";
|
|
|
24
24
|
import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
|
|
25
25
|
import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
|
|
26
26
|
import { validateHookDocument } from "./hook-schema.js";
|
|
27
|
+
import { HOOK_EVENTS_BY_HARNESS } from "./content/hook-events.js";
|
|
27
28
|
import { validateKnowledgeEntry } from "./knowledge-store.js";
|
|
28
29
|
import { readSeedShelf } from "./content/seed-shelf.js";
|
|
29
30
|
import { evaluateRetroGate } from "./retro-gate.js";
|
|
@@ -130,6 +131,20 @@ async function generatedCliEntrypointsOk(projectRoot) {
|
|
|
130
131
|
: "local CLI entrypoint check skipped because generated hook scripts are absent"
|
|
131
132
|
};
|
|
132
133
|
}
|
|
134
|
+
function expectedArtifactPrefix(stage) {
|
|
135
|
+
const index = FLOW_STAGES.indexOf(stage);
|
|
136
|
+
return `${String(index + 1).padStart(2, "0")}-`;
|
|
137
|
+
}
|
|
138
|
+
function artifactStageFromFileName(fileName) {
|
|
139
|
+
if (!fileName.endsWith(".md"))
|
|
140
|
+
return null;
|
|
141
|
+
for (const stage of FLOW_STAGES) {
|
|
142
|
+
if (fileName.startsWith(expectedArtifactPrefix(stage))) {
|
|
143
|
+
return stage;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
133
148
|
function extractUserPromptFromIdeaArtifact(markdown) {
|
|
134
149
|
const normalized = markdown.replace(/\r\n?/gu, "\n");
|
|
135
150
|
const heading = /^##\s+User prompt\s*$/imu.exec(normalized);
|
|
@@ -295,8 +310,12 @@ function opencodeConfigCandidates(projectRoot) {
|
|
|
295
310
|
return [
|
|
296
311
|
path.join(projectRoot, "opencode.json"),
|
|
297
312
|
path.join(projectRoot, "opencode.jsonc"),
|
|
313
|
+
path.join(projectRoot, "oh-my-opencode.jsonc"),
|
|
314
|
+
path.join(projectRoot, "oh-my-openagent.jsonc"),
|
|
298
315
|
path.join(projectRoot, ".opencode/opencode.json"),
|
|
299
|
-
path.join(projectRoot, ".opencode/opencode.jsonc")
|
|
316
|
+
path.join(projectRoot, ".opencode/opencode.jsonc"),
|
|
317
|
+
path.join(projectRoot, ".opencode/oh-my-opencode.jsonc"),
|
|
318
|
+
path.join(projectRoot, ".opencode/oh-my-openagent.jsonc")
|
|
300
319
|
];
|
|
301
320
|
}
|
|
302
321
|
function openCodeConfigRegistersPlugin(parsed) {
|
|
@@ -360,6 +379,87 @@ function opencodeQuestionEnvCheck() {
|
|
|
360
379
|
details: "Set OPENCODE_ENABLE_QUESTION_TOOL=1 for OpenCode ACP clients so permission-gated structured questions can use the question tool."
|
|
361
380
|
};
|
|
362
381
|
}
|
|
382
|
+
function codexFlagInactiveDetail(configPath, state, error) {
|
|
383
|
+
if (state === "enabled") {
|
|
384
|
+
return `codex_hooks feature flag is enabled in ${configPath}; Codex hooks are active.`;
|
|
385
|
+
}
|
|
386
|
+
if (state === "read-error") {
|
|
387
|
+
return `Codex hooks are inactive: could not read ${configPath} (${error instanceof Error ? error.message : String(error)}).`;
|
|
388
|
+
}
|
|
389
|
+
if (state === "missing-file") {
|
|
390
|
+
return `Codex hooks are inactive: ${configPath} does not exist; .codex/hooks.json is ignored until [features] codex_hooks = true is configured.`;
|
|
391
|
+
}
|
|
392
|
+
if (state === "missing-section") {
|
|
393
|
+
return `Codex hooks are inactive: ${configPath} has no [features] section; add codex_hooks = true to activate configured hooks.`;
|
|
394
|
+
}
|
|
395
|
+
if (state === "missing-key") {
|
|
396
|
+
return `Codex hooks are inactive: ${configPath} is missing codex_hooks under [features]; add codex_hooks = true to activate configured hooks.`;
|
|
397
|
+
}
|
|
398
|
+
return `Codex hooks are inactive: ${configPath} sets codex_hooks to a non-true value; set codex_hooks = true under [features].`;
|
|
399
|
+
}
|
|
400
|
+
function hookCommandsWithMatchers(value) {
|
|
401
|
+
if (!Array.isArray(value)) {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const out = [];
|
|
405
|
+
for (const item of value) {
|
|
406
|
+
const obj = toObject(item);
|
|
407
|
+
if (!obj)
|
|
408
|
+
continue;
|
|
409
|
+
const matcher = typeof obj.matcher === "string" ? obj.matcher : undefined;
|
|
410
|
+
if (typeof obj.command === "string") {
|
|
411
|
+
out.push({ command: obj.command, matcher });
|
|
412
|
+
}
|
|
413
|
+
const nested = hookCommandsWithMatchers(obj.hooks);
|
|
414
|
+
for (const child of nested) {
|
|
415
|
+
out.push({ ...child, matcher: child.matcher ?? matcher });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
function commandHasHandler(entries, handler) {
|
|
421
|
+
return entries.some((entry) => entry.command.includes(`run-hook.cmd ${handler}`) || entry.command.includes(`run-hook.mjs ${handler}`));
|
|
422
|
+
}
|
|
423
|
+
function codexBashOnly(entries, handler) {
|
|
424
|
+
const matches = entries.filter((entry) => entry.command.includes(`run-hook.cmd ${handler}`) || entry.command.includes(`run-hook.mjs ${handler}`));
|
|
425
|
+
return matches.length > 0 && matches.every((entry) => entry.matcher === "Bash|bash");
|
|
426
|
+
}
|
|
427
|
+
function codexStructuralWiringCheck(codexHooks) {
|
|
428
|
+
const problems = [];
|
|
429
|
+
const expectedSession = HOOK_EVENTS_BY_HARNESS.codex.session_rehydrate;
|
|
430
|
+
if (expectedSession !== "SessionStart matcher=startup|resume") {
|
|
431
|
+
problems.push("semantic session_rehydrate mapping must remain SessionStart matcher=startup|resume");
|
|
432
|
+
}
|
|
433
|
+
const session = hookCommandsWithMatchers(codexHooks.SessionStart);
|
|
434
|
+
if (!commandHasHandler(session, "session-start") || !session.some((entry) => entry.matcher === "startup|resume")) {
|
|
435
|
+
problems.push("SessionStart must run session-start with matcher startup|resume");
|
|
436
|
+
}
|
|
437
|
+
const userPrompt = hookCommandsWithMatchers(codexHooks.UserPromptSubmit);
|
|
438
|
+
if (!commandHasHandler(userPrompt, "prompt-guard")) {
|
|
439
|
+
problems.push("UserPromptSubmit must run prompt-guard");
|
|
440
|
+
}
|
|
441
|
+
if (!commandHasHandler(userPrompt, "verify-current-state")) {
|
|
442
|
+
problems.push("UserPromptSubmit must run verify-current-state");
|
|
443
|
+
}
|
|
444
|
+
const pre = hookCommandsWithMatchers(codexHooks.PreToolUse);
|
|
445
|
+
if (!codexBashOnly(pre, "prompt-guard")) {
|
|
446
|
+
problems.push("PreToolUse prompt-guard must be Bash-only matcher Bash|bash");
|
|
447
|
+
}
|
|
448
|
+
if (!codexBashOnly(pre, "workflow-guard")) {
|
|
449
|
+
problems.push("PreToolUse workflow-guard must be Bash-only matcher Bash|bash");
|
|
450
|
+
}
|
|
451
|
+
const post = hookCommandsWithMatchers(codexHooks.PostToolUse);
|
|
452
|
+
if (!codexBashOnly(post, "context-monitor")) {
|
|
453
|
+
problems.push("PostToolUse context-monitor must be Bash-only matcher Bash|bash");
|
|
454
|
+
}
|
|
455
|
+
const stop = hookCommandsWithMatchers(codexHooks.Stop);
|
|
456
|
+
if (!commandHasHandler(stop, "stop-handoff")) {
|
|
457
|
+
problems.push("Stop must run stop-handoff");
|
|
458
|
+
}
|
|
459
|
+
return problems.length === 0
|
|
460
|
+
? { ok: true, details: "Codex hook events, matchers, and manifest semantic mappings are structurally valid" }
|
|
461
|
+
: { ok: false, details: problems.join("; ") };
|
|
462
|
+
}
|
|
363
463
|
async function initRecoveryCheck(projectRoot) {
|
|
364
464
|
const sentinelPath = path.join(projectRoot, RUNTIME_ROOT, "state", ".init-in-progress");
|
|
365
465
|
if (!(await exists(sentinelPath))) {
|
|
@@ -1003,34 +1103,48 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1003
1103
|
ok: codexWiringOk,
|
|
1004
1104
|
details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt/verify-current-state), Bash-only PreToolUse(prompt/workflow), Bash-only PostToolUse(context-monitor), and Stop(stop-handoff). Codex workflow-guard is intentionally strict Bash-only.`
|
|
1005
1105
|
});
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1106
|
+
const codexStructural = codexStructuralWiringCheck(codexHooks);
|
|
1107
|
+
checks.push({
|
|
1108
|
+
name: "hook:wiring:codex:structure",
|
|
1109
|
+
ok: codexStructural.ok,
|
|
1110
|
+
details: codexStructural.details
|
|
1111
|
+
});
|
|
1112
|
+
// Codex ignores `.codex/hooks.json` unless the user has
|
|
1113
|
+
// `[features] codex_hooks = true` in `~/.codex/config.toml`.
|
|
1010
1114
|
const codexConfig = codexConfigPath();
|
|
1011
|
-
let
|
|
1115
|
+
let codexFlagState = "read-error";
|
|
1116
|
+
let codexFlagReadError;
|
|
1012
1117
|
try {
|
|
1013
1118
|
const content = await readCodexConfig(codexConfig);
|
|
1014
|
-
|
|
1015
|
-
featureFlagNote =
|
|
1016
|
-
state === "enabled"
|
|
1017
|
-
? `codex_hooks feature flag is enabled in ${codexConfig}`
|
|
1018
|
-
: state === "missing-file"
|
|
1019
|
-
? `warning: ${codexConfig} does not exist; .codex/hooks.json will be ignored until you create it with \`[features]\\ncodex_hooks = true\\n\`.`
|
|
1020
|
-
: state === "missing-section"
|
|
1021
|
-
? `warning: ${codexConfig} has no [features] section; add \`[features]\\ncodex_hooks = true\\n\` to enable cclaw hooks.`
|
|
1022
|
-
: state === "missing-key"
|
|
1023
|
-
? `warning: ${codexConfig} is missing the codex_hooks key under [features]. Add \`codex_hooks = true\` to enable cclaw hooks.`
|
|
1024
|
-
: `warning: ${codexConfig} sets codex_hooks to a non-true value; set \`codex_hooks = true\` under [features] to enable cclaw hooks.`;
|
|
1119
|
+
codexFlagState = classifyCodexHooksFlag(content);
|
|
1025
1120
|
}
|
|
1026
1121
|
catch (err) {
|
|
1027
|
-
|
|
1122
|
+
codexFlagReadError = err;
|
|
1028
1123
|
}
|
|
1124
|
+
const featureFlagNote = codexFlagInactiveDetail(codexConfig, codexFlagState, codexFlagReadError);
|
|
1125
|
+
const featureFlagOk = codexFlagState === "enabled";
|
|
1029
1126
|
checks.push({
|
|
1030
1127
|
name: "warning:codex:feature_flag",
|
|
1031
|
-
ok:
|
|
1032
|
-
details: featureFlagNote
|
|
1128
|
+
ok: featureFlagOk,
|
|
1129
|
+
details: featureFlagNote,
|
|
1130
|
+
summary: featureFlagOk
|
|
1131
|
+
? "Codex hooks are active."
|
|
1132
|
+
: "Codex hooks are inactive; configured hooks will be ignored.",
|
|
1133
|
+
fix: "Set `[features] codex_hooks = true` in the Codex config or run cclaw init/sync with Codex flag repair.",
|
|
1134
|
+
docRef: "docs/harnesses.md"
|
|
1033
1135
|
});
|
|
1136
|
+
if (parsedConfig?.strictness === "strict") {
|
|
1137
|
+
checks.push({
|
|
1138
|
+
name: "hook:codex:feature_flag_active",
|
|
1139
|
+
ok: featureFlagOk,
|
|
1140
|
+
details: featureFlagNote,
|
|
1141
|
+
summary: featureFlagOk
|
|
1142
|
+
? "Codex hooks are active for strict runtime enforcement."
|
|
1143
|
+
: "Codex hooks are inactive; strict Codex hook enforcement is not ready.",
|
|
1144
|
+
fix: "Set `[features] codex_hooks = true` in the Codex config so strict Codex hooks can run.",
|
|
1145
|
+
docRef: "docs/harnesses.md"
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1034
1148
|
// Legacy `.codex/commands/*` must not linger from older cclaw installs.
|
|
1035
1149
|
// (The `.codex/hooks.json` path is now managed and is validated above,
|
|
1036
1150
|
// so there is no longer a legacy_hooks_json warning.)
|
|
@@ -1530,13 +1644,21 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1530
1644
|
});
|
|
1531
1645
|
const artifactsRoot = path.join(projectRoot, RUNTIME_ROOT, "artifacts");
|
|
1532
1646
|
let artifactPlaceholderHits = [];
|
|
1647
|
+
let duplicateArtifactGroups = [];
|
|
1533
1648
|
if (await exists(artifactsRoot)) {
|
|
1534
1649
|
try {
|
|
1535
1650
|
const entries = await fs.readdir(artifactsRoot, { withFileTypes: true });
|
|
1536
1651
|
const placeholderPattern = /\b(?:TODO|TBD|FIXME)\b|<fill-in>|<your-.*-here>/giu;
|
|
1652
|
+
const stageArtifactFiles = new Map();
|
|
1537
1653
|
for (const entry of entries) {
|
|
1538
1654
|
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
1539
1655
|
continue;
|
|
1656
|
+
const stageForArtifact = artifactStageFromFileName(entry.name);
|
|
1657
|
+
if (stageForArtifact) {
|
|
1658
|
+
const files = stageArtifactFiles.get(stageForArtifact) ?? [];
|
|
1659
|
+
files.push(entry.name);
|
|
1660
|
+
stageArtifactFiles.set(stageForArtifact, files);
|
|
1661
|
+
}
|
|
1540
1662
|
const filePath = path.join(artifactsRoot, entry.name);
|
|
1541
1663
|
const content = await fs.readFile(filePath, "utf8");
|
|
1542
1664
|
const matchCount = (content.match(placeholderPattern) ?? []).length;
|
|
@@ -1544,9 +1666,13 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1544
1666
|
artifactPlaceholderHits.push(`${entry.name}:${matchCount}`);
|
|
1545
1667
|
}
|
|
1546
1668
|
}
|
|
1669
|
+
duplicateArtifactGroups = [...stageArtifactFiles.entries()]
|
|
1670
|
+
.filter(([, files]) => files.length > 1)
|
|
1671
|
+
.map(([stageName, files]) => `${stageName}: ${files.sort().join(", ")}`);
|
|
1547
1672
|
}
|
|
1548
1673
|
catch {
|
|
1549
1674
|
artifactPlaceholderHits = [];
|
|
1675
|
+
duplicateArtifactGroups = [];
|
|
1550
1676
|
}
|
|
1551
1677
|
}
|
|
1552
1678
|
checks.push({
|
|
@@ -1556,13 +1682,20 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1556
1682
|
? "no TODO/TBD/FIXME placeholder markers found in active artifacts"
|
|
1557
1683
|
: `warning: placeholder markers detected in active artifacts (${artifactPlaceholderHits.join(", ")}). Clear before marking completion.`
|
|
1558
1684
|
});
|
|
1685
|
+
checks.push({
|
|
1686
|
+
name: "warning:artifacts:duplicate_stage_artifacts",
|
|
1687
|
+
ok: duplicateArtifactGroups.length === 0,
|
|
1688
|
+
details: duplicateArtifactGroups.length === 0
|
|
1689
|
+
? "no duplicate stage artifacts detected in active artifacts"
|
|
1690
|
+
: `warning: duplicate stage artifacts detected (${duplicateArtifactGroups.join("; ")}). The resolver uses the newest matching file; archive or rename stale copies to avoid ambiguous operator handoff.`
|
|
1691
|
+
});
|
|
1559
1692
|
const staleStages = Object.keys(flowState.staleStages).filter((value) => FLOW_STAGES.includes(value));
|
|
1560
1693
|
checks.push({
|
|
1561
1694
|
name: "state:stale_stages_resolved",
|
|
1562
1695
|
ok: staleStages.length === 0,
|
|
1563
1696
|
details: staleStages.length === 0
|
|
1564
1697
|
? "no stale stages pending acknowledgement"
|
|
1565
|
-
: `stale stages pending acknowledgement: ${staleStages.join(", ")}
|
|
1698
|
+
: `stale stages pending acknowledgement: ${staleStages.join(", ")}. Re-run the current stale stage, then clear it with cclaw internal rewind --ack ${flowState.currentStage}.`
|
|
1566
1699
|
});
|
|
1567
1700
|
const retroGateStatus = await evaluateRetroGate(projectRoot, flowState);
|
|
1568
1701
|
checks.push({
|
|
@@ -1632,18 +1765,27 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1632
1765
|
ok: archiveIntegrity.ok,
|
|
1633
1766
|
details: archiveIntegrity.details
|
|
1634
1767
|
});
|
|
1768
|
+
const currentGateState = flowState.stageGateCatalog[flowState.currentStage];
|
|
1769
|
+
const currentStageUntouched = flowState.completedStages.length === 0 &&
|
|
1770
|
+
flowState.rewinds.length === 0 &&
|
|
1771
|
+
Object.keys(flowState.guardEvidence).length === 0 &&
|
|
1772
|
+
(currentGateState?.passed.length ?? 0) === 0 &&
|
|
1773
|
+
(currentGateState?.blocked.length ?? 0) === 0;
|
|
1635
1774
|
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
|
|
1636
1775
|
repairFeatureSystem: false
|
|
1637
1776
|
});
|
|
1777
|
+
const delegationSatisfiedForDoctor = currentStageUntouched || delegation.satisfied;
|
|
1638
1778
|
const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
|
|
1639
1779
|
? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
|
|
1640
1780
|
: "";
|
|
1641
1781
|
checks.push({
|
|
1642
1782
|
name: "delegation:mandatory:current_stage",
|
|
1643
|
-
ok:
|
|
1644
|
-
details:
|
|
1645
|
-
? `
|
|
1646
|
-
:
|
|
1783
|
+
ok: delegationSatisfiedForDoctor,
|
|
1784
|
+
details: currentStageUntouched
|
|
1785
|
+
? `mandatory delegation check deferred for untouched stage "${flowState.currentStage}"; stage-complete enforces it when work begins`
|
|
1786
|
+
: delegation.satisfied
|
|
1787
|
+
? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
|
|
1788
|
+
: `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
|
|
1647
1789
|
});
|
|
1648
1790
|
checks.push({
|
|
1649
1791
|
name: "warning:delegation:waived",
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export interface CloseoutState {
|
|
|
63
63
|
retroSkipReason?: string;
|
|
64
64
|
compoundCompletedAt?: string;
|
|
65
65
|
compoundSkipped?: boolean;
|
|
66
|
+
compoundSkipReason?: string;
|
|
66
67
|
compoundPromoted: number;
|
|
67
68
|
}
|
|
68
69
|
export declare function createInitialCloseoutState(): CloseoutState;
|
package/dist/flow-state.js
CHANGED
package/dist/gate-evidence.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
|
9
9
|
import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
|
|
10
10
|
import { readFlowState, writeFlowState } from "./runs.js";
|
|
11
11
|
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
12
|
+
import { validateTddVerificationEvidence } from "./tdd-verification-evidence.js";
|
|
12
13
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
13
14
|
import { FLOW_STAGES } from "./types.js";
|
|
14
15
|
async function currentStageArtifactExists(projectRoot, stage, track) {
|
|
@@ -36,6 +37,22 @@ async function readArtifactMarkdown(projectRoot, artifactFile) {
|
|
|
36
37
|
}
|
|
37
38
|
return null;
|
|
38
39
|
}
|
|
40
|
+
async function readStageArtifactMarkdown(projectRoot, stage, track) {
|
|
41
|
+
const resolved = await resolveArtifactPath(stage, {
|
|
42
|
+
projectRoot,
|
|
43
|
+
track,
|
|
44
|
+
intent: "read"
|
|
45
|
+
});
|
|
46
|
+
if (!(await exists(resolved.absPath))) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return await fs.readFile(resolved.absPath, "utf8");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
39
56
|
function unique(values) {
|
|
40
57
|
return [...new Set(values)];
|
|
41
58
|
}
|
|
@@ -263,6 +280,12 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
263
280
|
issues.push(`passed gate "${gateId}" is missing guardEvidence entry.`);
|
|
264
281
|
continue;
|
|
265
282
|
}
|
|
283
|
+
if (stage === "tdd" && gateId === "tdd_verified_before_complete") {
|
|
284
|
+
const verification = await validateTddVerificationEvidence(projectRoot, evidence);
|
|
285
|
+
if (!verification.ok) {
|
|
286
|
+
issues.push(`tdd verification gate blocked (${gateId}): ${verification.issues.join(" ")}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
266
289
|
const discoveredCommandIssue = await verifyDiscoveredCommandEvidence(projectRoot, stage, gateId, flowState);
|
|
267
290
|
if (discoveredCommandIssue) {
|
|
268
291
|
issues.push(discoveredCommandIssue);
|
|
@@ -336,7 +359,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
336
359
|
if (stage === "design") {
|
|
337
360
|
const researchGateRequired = schema.requiredGates.some((gate) => gate.id === "design_research_complete" && gate.tier === "required");
|
|
338
361
|
if (researchGateRequired) {
|
|
339
|
-
const designMarkdown = await
|
|
362
|
+
const designMarkdown = await readStageArtifactMarkdown(projectRoot, "design", flowState.track);
|
|
340
363
|
const inlineResearchBody = designMarkdown
|
|
341
364
|
? extractMarkdownSectionBody(designMarkdown, "Research Fleet Synthesis")
|
|
342
365
|
: null;
|
|
@@ -354,7 +377,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
354
377
|
const inlineResearchComplete = inlineResearchLines.length > 0;
|
|
355
378
|
const researchMarkdown = await readArtifactMarkdown(projectRoot, "02a-research.md");
|
|
356
379
|
if (!inlineResearchComplete && !researchMarkdown) {
|
|
357
|
-
issues.push("design research gate blocked (design_research_complete): fill `Research Fleet Synthesis` in
|
|
380
|
+
issues.push("design research gate blocked (design_research_complete): fill `Research Fleet Synthesis` in the active design artifact, or write `.cclaw/artifacts/02a-research.md` for deep/high-risk research.");
|
|
358
381
|
}
|
|
359
382
|
else if (researchMarkdown) {
|
|
360
383
|
const missingSections = [];
|
package/dist/install.js
CHANGED
|
@@ -23,6 +23,7 @@ import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
|
|
|
23
23
|
import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LANGUAGE_RULE_PACK_GENERATORS, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
|
|
24
24
|
import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
|
|
25
25
|
import { SUBAGENT_CONTEXT_SKILLS } from "./content/subagent-context-skills.js";
|
|
26
|
+
import { CCLAW_AGENTS } from "./content/core-agents.js";
|
|
26
27
|
import { createInitialFlowState } from "./flow-state.js";
|
|
27
28
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
28
29
|
import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
@@ -191,15 +192,84 @@ function resolveRepoRoot() {
|
|
|
191
192
|
return process.cwd();
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
function isZeroSha(value) {
|
|
196
|
+
return /^0{40,64}$/u.test(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readStdin() {
|
|
200
|
+
try {
|
|
201
|
+
return fs.readFileSync(0, "utf8");
|
|
202
|
+
} catch {
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function uniqueLines(chunks) {
|
|
208
|
+
return [...new Set(chunks
|
|
209
|
+
.join("\n")
|
|
210
|
+
.split(/\r?\n/gu)
|
|
211
|
+
.map((line) => line.trim())
|
|
212
|
+
.filter((line) => line.length > 0))].join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function diffNames(root, range) {
|
|
216
|
+
const result = runGit(["diff", "--name-only", range], root);
|
|
217
|
+
return result.status === 0 ? result.stdout : "";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function changedFilesFromUnpushedCommits(root, localSha = "HEAD") {
|
|
221
|
+
const revList = runGit(["rev-list", "--reverse", localSha, "--not", "--remotes"], root);
|
|
222
|
+
if (revList.status !== 0 || revList.stdout.trim().length === 0) {
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
const chunks = [];
|
|
226
|
+
for (const commit of revList.stdout.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean)) {
|
|
227
|
+
const diffTree = runGit(["diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commit], root);
|
|
228
|
+
if (diffTree.status === 0) chunks.push(diffTree.stdout);
|
|
229
|
+
}
|
|
230
|
+
return uniqueLines(chunks);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function changedFilesFromPrePushStdin(root, stdin) {
|
|
234
|
+
const chunks = [];
|
|
235
|
+
for (const rawLine of stdin.split(/\r?\n/gu)) {
|
|
236
|
+
const parts = rawLine.trim().split(/\s+/u);
|
|
237
|
+
if (parts.length < 4) continue;
|
|
238
|
+
const [localRef, localSha, remoteRef, remoteSha] = parts;
|
|
239
|
+
void localRef;
|
|
240
|
+
void remoteRef;
|
|
241
|
+
if (!localSha || isZeroSha(localSha)) continue;
|
|
242
|
+
if (remoteSha && !isZeroSha(remoteSha)) {
|
|
243
|
+
chunks.push(diffNames(root, remoteSha + ".." + localSha));
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const upstream = runGit(["rev-parse", "--verify", "--quiet", "@{upstream}"], root);
|
|
247
|
+
if (upstream.status === 0 && upstream.stdout.trim().length > 0) {
|
|
248
|
+
chunks.push(diffNames(root, upstream.stdout.trim() + ".." + localSha));
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
chunks.push(changedFilesFromUnpushedCommits(root, localSha));
|
|
252
|
+
}
|
|
253
|
+
return uniqueLines(chunks);
|
|
254
|
+
}
|
|
255
|
+
|
|
194
256
|
function resolveChangedFiles(root) {
|
|
195
257
|
if (HOOK_NAME === "pre-commit") {
|
|
196
258
|
const result = runGit(["diff", "--cached", "--name-only"], root);
|
|
197
259
|
return result.status === 0 ? result.stdout : "";
|
|
198
260
|
}
|
|
199
|
-
const
|
|
261
|
+
const stdinChanged = changedFilesFromPrePushStdin(root, readStdin());
|
|
262
|
+
if (stdinChanged.length > 0) {
|
|
263
|
+
return stdinChanged;
|
|
264
|
+
}
|
|
265
|
+
const upstreamResult = runGit(["diff", "--name-only", "@{upstream}..HEAD"], root);
|
|
200
266
|
if (upstreamResult.status === 0) {
|
|
201
267
|
return upstreamResult.stdout;
|
|
202
268
|
}
|
|
269
|
+
const unpushed = changedFilesFromUnpushedCommits(root);
|
|
270
|
+
if (unpushed.length > 0) {
|
|
271
|
+
return unpushed;
|
|
272
|
+
}
|
|
203
273
|
const fallback = runGit(["diff", "--name-only", "HEAD~1...HEAD"], root);
|
|
204
274
|
return fallback.status === 0 ? fallback.stdout : "";
|
|
205
275
|
}
|
|
@@ -1270,13 +1340,7 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
1270
1340
|
}
|
|
1271
1341
|
await removeIfEmpty(codexSkillsRoot);
|
|
1272
1342
|
await removeIfEmpty(path.join(projectRoot, ".agents"));
|
|
1273
|
-
const managedAgentNames =
|
|
1274
|
-
"planner",
|
|
1275
|
-
"reviewer",
|
|
1276
|
-
"security-reviewer",
|
|
1277
|
-
"test-author",
|
|
1278
|
-
"doc-updater"
|
|
1279
|
-
];
|
|
1343
|
+
const managedAgentNames = CCLAW_AGENTS.map((agent) => agent.name);
|
|
1280
1344
|
for (const agentName of managedAgentNames) {
|
|
1281
1345
|
await removeBestEffort(path.join(projectRoot, ".opencode/agents", `${agentName}.md`));
|
|
1282
1346
|
await removeBestEffort(path.join(projectRoot, ".codex/agents", `${agentName}.toml`));
|