cclaw-cli 0.48.0 → 0.48.1
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.d.ts +2 -2
- package/dist/artifact-linter.js +2 -2
- package/dist/content/core-agents.d.ts +53 -1
- package/dist/content/core-agents.js +6 -0
- package/dist/content/observe.js +22 -1
- package/dist/content/opencode-plugin.js +5 -1
- package/dist/content/stage-schema.js +2 -0
- package/dist/content/templates.js +11 -10
- package/dist/content/utility-skills.d.ts +7 -1
- package/dist/content/utility-skills.js +5 -0
- package/dist/delegation.d.ts +10 -0
- package/dist/delegation.js +13 -1
- package/dist/doctor.js +4 -4
- package/dist/flow-state.d.ts +1 -1
- package/dist/flow-state.js +15 -1
- package/dist/fs-utils.d.ts +9 -0
- package/dist/fs-utils.js +35 -1
- package/dist/gate-evidence.js +1 -1
- package/dist/gitignore.js +6 -3
- package/dist/harness-adapters.js +11 -1
- package/dist/install.js +40 -4
- package/dist/knowledge-store.js +2 -2
- package/dist/retro-gate.js +23 -14
- package/dist/run-archive.js +63 -33
- package/dist/run-persistence.js +7 -0
- package/dist/tdd-cycle.js +6 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FlowStage } from "./types.js";
|
|
1
|
+
import { type FlowStage, type FlowTrack } from "./types.js";
|
|
2
2
|
export interface LintFinding {
|
|
3
3
|
section: string;
|
|
4
4
|
required: boolean;
|
|
@@ -46,7 +46,7 @@ export interface LearningsParseResult {
|
|
|
46
46
|
details: string;
|
|
47
47
|
}
|
|
48
48
|
export declare function parseLearningsSection(sectionBody: string): LearningsParseResult;
|
|
49
|
-
export declare function lintArtifact(projectRoot: string, stage: FlowStage): Promise<LintResult>;
|
|
49
|
+
export declare function lintArtifact(projectRoot: string, stage: FlowStage, track?: FlowTrack): Promise<LintResult>;
|
|
50
50
|
export declare function validateReviewArmy(projectRoot: string): Promise<{
|
|
51
51
|
valid: boolean;
|
|
52
52
|
errors: string[];
|
package/dist/artifact-linter.js
CHANGED
|
@@ -795,8 +795,8 @@ function validateSectionBody(sectionBody, rule, sectionName) {
|
|
|
795
795
|
details: "Section heading and content satisfy lint heuristics."
|
|
796
796
|
};
|
|
797
797
|
}
|
|
798
|
-
export async function lintArtifact(projectRoot, stage) {
|
|
799
|
-
const schema = stageSchema(stage);
|
|
798
|
+
export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
799
|
+
const schema = stageSchema(stage, track);
|
|
800
800
|
const { absPath: absFile, relPath: relFile } = await resolveArtifactPath(projectRoot, schema.artifactFile);
|
|
801
801
|
const findings = [];
|
|
802
802
|
if (!(await exists(absFile))) {
|
|
@@ -24,8 +24,60 @@ export interface AgentDefinition {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Canonical specialist roster (core-5) materialized under `.cclaw/agents/`.
|
|
27
|
+
*
|
|
28
|
+
* Declared with `satisfies` so the array retains literal `name` types for
|
|
29
|
+
* downstream type-level consumers (e.g. `AgentName`), while still being
|
|
30
|
+
* checked against the `AgentDefinition` shape at compile time. Do not add
|
|
31
|
+
* an explicit `AgentDefinition[]` annotation here — it would widen `name`
|
|
32
|
+
* to `string` and break the compile-time drift guard.
|
|
33
|
+
*/
|
|
34
|
+
export declare const CCLAW_AGENTS: readonly [{
|
|
35
|
+
readonly name: "planner";
|
|
36
|
+
readonly description: "MANDATORY for scope/design/plan and PROACTIVE for high-ambiguity work. MUST BE USED when sequencing, dependency mapping, or risk trade-offs are required before coding.";
|
|
37
|
+
readonly tools: ["Read", "Grep", "Glob", "WebSearch"];
|
|
38
|
+
readonly model: "deep";
|
|
39
|
+
readonly activation: "mandatory";
|
|
40
|
+
readonly relatedStages: ["brainstorm", "scope", "design", "spec", "plan"];
|
|
41
|
+
readonly body: string;
|
|
42
|
+
}, {
|
|
43
|
+
readonly name: "reviewer";
|
|
44
|
+
readonly description: "MANDATORY during review. MUST BE USED to run a two-pass audit: spec compliance first, then correctness/maintainability/performance/architecture.";
|
|
45
|
+
readonly tools: ["Read", "Grep", "Glob"];
|
|
46
|
+
readonly model: "balanced";
|
|
47
|
+
readonly activation: "mandatory";
|
|
48
|
+
readonly relatedStages: ["spec", "review", "ship"];
|
|
49
|
+
readonly body: string;
|
|
50
|
+
}, {
|
|
51
|
+
readonly name: "security-reviewer";
|
|
52
|
+
readonly description: "MANDATORY during review; PROACTIVE during design/ship for trust-boundary changes. Always produce an explicit no-change attestation when no security-relevant surface moved.";
|
|
53
|
+
readonly tools: ["Read", "Grep", "Glob"];
|
|
54
|
+
readonly model: "balanced";
|
|
55
|
+
readonly activation: "mandatory";
|
|
56
|
+
readonly relatedStages: ["design", "review", "ship"];
|
|
57
|
+
readonly body: string;
|
|
58
|
+
}, {
|
|
59
|
+
readonly name: "test-author";
|
|
60
|
+
readonly description: "MANDATORY in TDD stage. MUST BE USED for RED -> GREEN -> REFACTOR with evidence-first discipline.";
|
|
61
|
+
readonly tools: ["Read", "Write", "Edit", "Grep", "Glob", "Bash"];
|
|
62
|
+
readonly model: "balanced";
|
|
63
|
+
readonly activation: "mandatory";
|
|
64
|
+
readonly relatedStages: ["tdd"];
|
|
65
|
+
readonly body: string;
|
|
66
|
+
}, {
|
|
67
|
+
readonly name: "doc-updater";
|
|
68
|
+
readonly description: "MANDATORY at ship and PROACTIVE when behavior/config/public API changes. Keep docs and runbooks in lockstep with shipped behavior.";
|
|
69
|
+
readonly tools: ["Read", "Write", "Edit", "Grep", "Glob"];
|
|
70
|
+
readonly model: "fast";
|
|
71
|
+
readonly activation: "mandatory";
|
|
72
|
+
readonly relatedStages: ["tdd", "ship"];
|
|
73
|
+
readonly body: string;
|
|
74
|
+
}];
|
|
75
|
+
/**
|
|
76
|
+
* Union of known agent names (compile-time). Use this in content that
|
|
77
|
+
* references agents by name so the TypeScript compiler catches renames
|
|
78
|
+
* and typos instead of letting them slip into generated artifacts.
|
|
27
79
|
*/
|
|
28
|
-
export
|
|
80
|
+
export type AgentName = (typeof CCLAW_AGENTS)[number]["name"];
|
|
29
81
|
/**
|
|
30
82
|
* Render a complete cclaw agent markdown file (YAML frontmatter + body).
|
|
31
83
|
*/
|
|
@@ -15,6 +15,12 @@ function yamlFlowSequence(values) {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Canonical specialist roster (core-5) materialized under `.cclaw/agents/`.
|
|
18
|
+
*
|
|
19
|
+
* Declared with `satisfies` so the array retains literal `name` types for
|
|
20
|
+
* downstream type-level consumers (e.g. `AgentName`), while still being
|
|
21
|
+
* checked against the `AgentDefinition` shape at compile time. Do not add
|
|
22
|
+
* an explicit `AgentDefinition[]` annotation here — it would widen `name`
|
|
23
|
+
* to `string` and break the compile-time drift guard.
|
|
18
24
|
*/
|
|
19
25
|
export const CCLAW_AGENTS = [
|
|
20
26
|
{
|
package/dist/content/observe.js
CHANGED
|
@@ -1094,11 +1094,20 @@ export function claudeHooksJsonWithObservation() {
|
|
|
1094
1094
|
}]
|
|
1095
1095
|
}],
|
|
1096
1096
|
PreToolUse: [{
|
|
1097
|
+
// `prompt-guard.sh` inspects tool inputs across all tool calls;
|
|
1098
|
+
// it has to stay on `*` so it sees MCP/Edit/Write/WebSearch
|
|
1099
|
+
// traffic too. `workflow-guard.sh`, however, only checks TDD
|
|
1100
|
+
// ordering on write-like operations — it is a no-op for reads.
|
|
1101
|
+
// Splitting the two matchers cuts Claude's per-read hook
|
|
1102
|
+
// overhead in half without reducing coverage on write paths.
|
|
1097
1103
|
matcher: "*",
|
|
1098
1104
|
hooks: [{
|
|
1099
1105
|
type: "command",
|
|
1100
1106
|
command: hookDispatcherCommand("prompt-guard.sh")
|
|
1101
|
-
}
|
|
1107
|
+
}]
|
|
1108
|
+
}, {
|
|
1109
|
+
matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
1110
|
+
hooks: [{
|
|
1102
1111
|
type: "command",
|
|
1103
1112
|
command: hookDispatcherCommand("workflow-guard.sh")
|
|
1104
1113
|
}]
|
|
@@ -1196,6 +1205,18 @@ export function codexHooksJsonWithObservation() {
|
|
|
1196
1205
|
hooks: [{
|
|
1197
1206
|
type: "command",
|
|
1198
1207
|
command: hookDispatcherCommand("prompt-guard.sh")
|
|
1208
|
+
}, {
|
|
1209
|
+
// `workflow-guard.sh` also runs here because Codex's PreToolUse
|
|
1210
|
+
// only sees Bash; Write/Edit/MCP writes never reach the hook
|
|
1211
|
+
// surface. Running workflow-guard on UserPromptSubmit catches
|
|
1212
|
+
// TDD-order violations that originate from the user's prompt
|
|
1213
|
+
// text (e.g. "edit X.ts to ..."). Payload is a prompt envelope,
|
|
1214
|
+
// not a tool call, so the script's TOOL extraction falls back
|
|
1215
|
+
// to "unknown" and advisory mode is a no-op by design — the
|
|
1216
|
+
// value is that prompt text is scanned for write-shaped intent
|
|
1217
|
+
// via the existing PAYLOAD_LOWER heuristics.
|
|
1218
|
+
type: "command",
|
|
1219
|
+
command: hookDispatcherCommand("workflow-guard.sh")
|
|
1199
1220
|
}, {
|
|
1200
1221
|
type: "command",
|
|
1201
1222
|
command: "bash -lc 'if command -v cclaw >/dev/null 2>&1; then cclaw internal verify-current-state --quiet >/dev/null || true; else npx -y cclaw-cli internal verify-current-state --quiet >/dev/null || true; fi'"
|
|
@@ -233,11 +233,15 @@ export default function cclawPlugin(ctx) {
|
|
|
233
233
|
eventType === "session.created" ||
|
|
234
234
|
eventType === "session.resumed" ||
|
|
235
235
|
eventType === "session.compacted" ||
|
|
236
|
-
eventType === "session.cleared"
|
|
236
|
+
eventType === "session.cleared" ||
|
|
237
|
+
eventType === "session.updated"
|
|
237
238
|
) {
|
|
238
239
|
// Avoid writing directly to stdout in lifecycle hooks because it can
|
|
239
240
|
// interfere with OpenCode TUI rendering. Bootstrap is injected via
|
|
240
241
|
// the system transform hook instead.
|
|
242
|
+
// session.updated covers config reloads and artifact/rules edits
|
|
243
|
+
// that happen mid-session; without it the cache would stay stale
|
|
244
|
+
// until the next compaction or restart.
|
|
241
245
|
refreshBootstrapCache();
|
|
242
246
|
}
|
|
243
247
|
if (eventType === "session.compacted") {
|
|
@@ -5,6 +5,7 @@ import { tddStageForTrack } from "./stages/tdd.js";
|
|
|
5
5
|
const ARTIFACT_STAGE_BY_PATH = {
|
|
6
6
|
".cclaw/artifacts/01-brainstorm.md": "brainstorm",
|
|
7
7
|
".cclaw/artifacts/02-scope.md": "scope",
|
|
8
|
+
".cclaw/artifacts/02a-research.md": "design",
|
|
8
9
|
".cclaw/artifacts/03-design.md": "design",
|
|
9
10
|
".cclaw/artifacts/04-spec.md": "spec",
|
|
10
11
|
".cclaw/artifacts/05-plan.md": "plan",
|
|
@@ -46,6 +47,7 @@ const REQUIRED_GATE_IDS = {
|
|
|
46
47
|
"tdd_green_full_suite",
|
|
47
48
|
"tdd_refactor_completed",
|
|
48
49
|
"tdd_verified_before_complete",
|
|
50
|
+
"tdd_docs_drift_check",
|
|
49
51
|
...(track === "quick" ? [] : ["tdd_traceable_to_plan"])
|
|
50
52
|
],
|
|
51
53
|
review: (track) => [
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { CCLAW_VERSION } from "../constants.js";
|
|
1
2
|
import { orderedStageSchemas } from "./stage-schema.js";
|
|
2
3
|
import { FLOW_STAGES } from "../types.js";
|
|
3
4
|
export const ARTIFACT_TEMPLATES = {
|
|
4
5
|
"01-brainstorm.md": `---
|
|
5
6
|
stage: brainstorm
|
|
6
7
|
schema_version: 1
|
|
7
|
-
version:
|
|
8
|
+
version: ${CCLAW_VERSION}
|
|
8
9
|
feature: <feature-id>
|
|
9
10
|
locked_decisions: []
|
|
10
11
|
inputs_hash: sha256:pending
|
|
@@ -52,7 +53,7 @@ inputs_hash: sha256:pending
|
|
|
52
53
|
"02-scope.md": `---
|
|
53
54
|
stage: scope
|
|
54
55
|
schema_version: 1
|
|
55
|
-
version:
|
|
56
|
+
version: ${CCLAW_VERSION}
|
|
56
57
|
feature: <feature-id>
|
|
57
58
|
locked_decisions: []
|
|
58
59
|
inputs_hash: sha256:pending
|
|
@@ -158,7 +159,7 @@ inputs_hash: sha256:pending
|
|
|
158
159
|
"02a-research.md": `---
|
|
159
160
|
stage: design
|
|
160
161
|
schema_version: 1
|
|
161
|
-
version:
|
|
162
|
+
version: ${CCLAW_VERSION}
|
|
162
163
|
feature: <feature-id>
|
|
163
164
|
locked_decisions: []
|
|
164
165
|
inputs_hash: sha256:pending
|
|
@@ -199,7 +200,7 @@ inputs_hash: sha256:pending
|
|
|
199
200
|
"03-design.md": `---
|
|
200
201
|
stage: design
|
|
201
202
|
schema_version: 1
|
|
202
|
-
version:
|
|
203
|
+
version: ${CCLAW_VERSION}
|
|
203
204
|
feature: <feature-id>
|
|
204
205
|
locked_decisions: []
|
|
205
206
|
inputs_hash: sha256:pending
|
|
@@ -303,7 +304,7 @@ inputs_hash: sha256:pending
|
|
|
303
304
|
"04-spec.md": `---
|
|
304
305
|
stage: spec
|
|
305
306
|
schema_version: 1
|
|
306
|
-
version:
|
|
307
|
+
version: ${CCLAW_VERSION}
|
|
307
308
|
feature: <feature-id>
|
|
308
309
|
locked_decisions: []
|
|
309
310
|
inputs_hash: sha256:pending
|
|
@@ -359,7 +360,7 @@ inputs_hash: sha256:pending
|
|
|
359
360
|
"05-plan.md": `---
|
|
360
361
|
stage: plan
|
|
361
362
|
schema_version: 1
|
|
362
|
-
version:
|
|
363
|
+
version: ${CCLAW_VERSION}
|
|
363
364
|
feature: <feature-id>
|
|
364
365
|
locked_decisions: []
|
|
365
366
|
inputs_hash: sha256:pending
|
|
@@ -438,7 +439,7 @@ Execution rule: complete and verify each batch before starting the next batch.
|
|
|
438
439
|
"06-tdd.md": `---
|
|
439
440
|
stage: tdd
|
|
440
441
|
schema_version: 1
|
|
441
|
-
version:
|
|
442
|
+
version: ${CCLAW_VERSION}
|
|
442
443
|
feature: <feature-id>
|
|
443
444
|
locked_decisions: []
|
|
444
445
|
inputs_hash: sha256:pending
|
|
@@ -505,7 +506,7 @@ inputs_hash: sha256:pending
|
|
|
505
506
|
"07-review.md": `---
|
|
506
507
|
stage: review
|
|
507
508
|
schema_version: 1
|
|
508
|
-
version:
|
|
509
|
+
version: ${CCLAW_VERSION}
|
|
509
510
|
feature: <feature-id>
|
|
510
511
|
locked_decisions: []
|
|
511
512
|
inputs_hash: sha256:pending
|
|
@@ -614,7 +615,7 @@ inputs_hash: sha256:pending
|
|
|
614
615
|
"08-ship.md": `---
|
|
615
616
|
stage: ship
|
|
616
617
|
schema_version: 1
|
|
617
|
-
version:
|
|
618
|
+
version: ${CCLAW_VERSION}
|
|
618
619
|
feature: <feature-id>
|
|
619
620
|
locked_decisions: []
|
|
620
621
|
inputs_hash: sha256:pending
|
|
@@ -669,7 +670,7 @@ inputs_hash: sha256:pending
|
|
|
669
670
|
"09-retro.md": `---
|
|
670
671
|
stage: retro
|
|
671
672
|
schema_version: 1
|
|
672
|
-
version:
|
|
673
|
+
version: ${CCLAW_VERSION}
|
|
673
674
|
feature: <feature-id>
|
|
674
675
|
locked_decisions: []
|
|
675
676
|
inputs_hash: sha256:pending
|
|
@@ -48,4 +48,10 @@ export declare const LANGUAGE_RULE_PACK_GENERATORS: Record<string, () => string>
|
|
|
48
48
|
*/
|
|
49
49
|
export declare const LEGACY_LANGUAGE_RULE_PACK_FOLDERS: readonly ["language-typescript", "language-python", "language-go"];
|
|
50
50
|
export declare const UTILITY_SKILL_FOLDERS: readonly ["security", "debugging", "performance", "ci-cd", "docs", "executing-plans", "verification-before-completion", "finishing-a-development-branch", "context-engineering", "source-driven-development", "frontend-accessibility", "landscape-check", "adversarial-review", "security-audit", "knowledge-curation", "retrospective", "document-review", "receiving-code-review"];
|
|
51
|
-
export
|
|
51
|
+
export type UtilitySkillFolder = (typeof UTILITY_SKILL_FOLDERS)[number];
|
|
52
|
+
/**
|
|
53
|
+
* One entry per `UTILITY_SKILL_FOLDERS` slot. Typed via the tuple so that
|
|
54
|
+
* adding a folder without a generator (or vice versa) is a TypeScript
|
|
55
|
+
* error — keeps the two sources of truth in lockstep at compile time.
|
|
56
|
+
*/
|
|
57
|
+
export declare const UTILITY_SKILL_MAP: Record<UtilitySkillFolder, () => string>;
|
|
@@ -1735,6 +1735,11 @@ export const UTILITY_SKILL_FOLDERS = [
|
|
|
1735
1735
|
"document-review",
|
|
1736
1736
|
"receiving-code-review"
|
|
1737
1737
|
];
|
|
1738
|
+
/**
|
|
1739
|
+
* One entry per `UTILITY_SKILL_FOLDERS` slot. Typed via the tuple so that
|
|
1740
|
+
* adding a folder without a generator (or vice versa) is a TypeScript
|
|
1741
|
+
* error — keeps the two sources of truth in lockstep at compile time.
|
|
1742
|
+
*/
|
|
1738
1743
|
export const UTILITY_SKILL_MAP = {
|
|
1739
1744
|
security: securityReviewSkill,
|
|
1740
1745
|
debugging: debuggingSkill,
|
package/dist/delegation.d.ts
CHANGED
|
@@ -66,6 +66,16 @@ export type DelegationLedger = {
|
|
|
66
66
|
runId: string;
|
|
67
67
|
entries: DelegationEntry[];
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Heuristic: does a changed file path strongly imply a trust-boundary
|
|
71
|
+
* surface? Used to gate adversarial-reviewer requirements on review.
|
|
72
|
+
*
|
|
73
|
+
* Matches authN/Z, credentials, crypto, policy, or explicit sanitization
|
|
74
|
+
* or injection handling. Intentionally excludes broad terms like `input`
|
|
75
|
+
* and `validation` because they match innocuous paths such as
|
|
76
|
+
* `form-input.ts` or `number-validation.ts` and produce false positives.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isTrustBoundaryPath(filePath: string): boolean;
|
|
69
79
|
export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
|
|
70
80
|
export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
|
|
71
81
|
/**
|
package/dist/delegation.js
CHANGED
|
@@ -53,6 +53,18 @@ async function resolveReviewDiffBase(projectRoot) {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Heuristic: does a changed file path strongly imply a trust-boundary
|
|
58
|
+
* surface? Used to gate adversarial-reviewer requirements on review.
|
|
59
|
+
*
|
|
60
|
+
* Matches authN/Z, credentials, crypto, policy, or explicit sanitization
|
|
61
|
+
* or injection handling. Intentionally excludes broad terms like `input`
|
|
62
|
+
* and `validation` because they match innocuous paths such as
|
|
63
|
+
* `form-input.ts` or `number-validation.ts` and produce false positives.
|
|
64
|
+
*/
|
|
65
|
+
export function isTrustBoundaryPath(filePath) {
|
|
66
|
+
return /(auth|security|secret|token|credential|permission|acl|policy|oauth|session|encrypt|decrypt|sanitize|untrusted|csrf|xss|injection|taint)/iu.test(filePath);
|
|
67
|
+
}
|
|
56
68
|
async function detectReviewTriggers(projectRoot) {
|
|
57
69
|
const empty = {
|
|
58
70
|
changedFiles: 0,
|
|
@@ -81,7 +93,7 @@ async function detectReviewTriggers(projectRoot) {
|
|
|
81
93
|
.split(/\r?\n/gu)
|
|
82
94
|
.map((line) => line.trim())
|
|
83
95
|
.filter((line) => line.length > 0);
|
|
84
|
-
const trustBoundaryChanged = changedPaths.some((
|
|
96
|
+
const trustBoundaryChanged = changedPaths.some((p) => isTrustBoundaryPath(p));
|
|
85
97
|
const requireAdversarialReviewer = changedLines > 100 || changedFiles > 10 || trustBoundaryChanged;
|
|
86
98
|
return {
|
|
87
99
|
changedFiles,
|
package/dist/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
|
|
6
|
+
import { REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
|
|
7
7
|
import { CCLAW_AGENTS } from "./content/core-agents.js";
|
|
8
8
|
import { detectAdvancedKeys, readConfig } from "./config.js";
|
|
9
9
|
import { exists } from "./fs-utils.js";
|
|
@@ -547,8 +547,8 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
547
547
|
ok: agentsBlockOk,
|
|
548
548
|
details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
|
|
549
549
|
});
|
|
550
|
-
// Utility commands
|
|
551
|
-
for (const cmd of
|
|
550
|
+
// Utility commands — keep in sync with UTILITY_COMMANDS (src/constants.ts)
|
|
551
|
+
for (const cmd of UTILITY_COMMANDS) {
|
|
552
552
|
const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
|
|
553
553
|
checks.push({
|
|
554
554
|
name: `utility_command:${cmd}`,
|
|
@@ -815,7 +815,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
815
815
|
});
|
|
816
816
|
checks.push({
|
|
817
817
|
name: `shim:codex:${skillName}:frontmatter`,
|
|
818
|
-
ok,
|
|
818
|
+
ok: frontmatterOk,
|
|
819
819
|
details: frontmatterOk
|
|
820
820
|
? `${skillPath} has \`name: ${skillName}\` frontmatter`
|
|
821
821
|
: ok
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -94,6 +94,6 @@ export declare function skippedStagesForTrack(track: FlowTrack): FlowStage[];
|
|
|
94
94
|
export declare function firstStageForTrack(track: FlowTrack): FlowStage;
|
|
95
95
|
export declare function createInitialFlowState(activeRunIdOrOptions?: string | InitialFlowStateOptions, maybeTrack?: FlowTrack): FlowState;
|
|
96
96
|
export declare function canTransition(from: FlowStage, to: FlowStage): boolean;
|
|
97
|
-
export declare function getTransitionGuards(from: FlowStage, to: FlowStage): string[];
|
|
97
|
+
export declare function getTransitionGuards(from: FlowStage, to: FlowStage, track?: FlowTrack): string[];
|
|
98
98
|
export declare function nextStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
|
99
99
|
export declare function previousStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
package/dist/flow-state.js
CHANGED
|
@@ -97,7 +97,21 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
|
|
|
97
97
|
export function canTransition(from, to) {
|
|
98
98
|
return TRANSITION_RULES.some((rule) => rule.from === from && rule.to === to);
|
|
99
99
|
}
|
|
100
|
-
export function getTransitionGuards(from, to) {
|
|
100
|
+
export function getTransitionGuards(from, to, track = "standard") {
|
|
101
|
+
// Natural forward edge on this track: derive guards fresh from the
|
|
102
|
+
// track-specific gate schema. `TRANSITION_RULES` collapses shared edges
|
|
103
|
+
// across tracks (first-registered wins), so reading guards directly
|
|
104
|
+
// from the track-aware schema avoids silently dropping gates that only
|
|
105
|
+
// the current track requires (e.g. `tdd_traceable_to_plan` on standard
|
|
106
|
+
// gets lost if quick was registered first).
|
|
107
|
+
const ordered = TRACK_STAGES[track];
|
|
108
|
+
const fromIdx = ordered.indexOf(from);
|
|
109
|
+
if (fromIdx >= 0 && ordered[fromIdx + 1] === to) {
|
|
110
|
+
return stageGateIds(from, track);
|
|
111
|
+
}
|
|
112
|
+
// Non-neighbour edges (e.g. `review -> tdd` with `review_verdict_blocked`)
|
|
113
|
+
// carry special guards not derivable from a stage's gate catalog; fall
|
|
114
|
+
// back to the pre-computed rule table.
|
|
101
115
|
const match = TRANSITION_RULES.find((rule) => rule.from === from && rule.to === to);
|
|
102
116
|
return match ? [...match.guards] : [];
|
|
103
117
|
}
|
package/dist/fs-utils.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* Strip a leading UTF-8 BOM (U+FEFF) if present. Many editors (VS Code on
|
|
4
|
+
* Windows, Notepad, some CI tools) silently prepend a BOM when saving
|
|
5
|
+
* UTF-8; when the file is then split on `\n` the first line keeps the
|
|
6
|
+
* invisible BOM and `JSON.parse` rejects it, which caused the first
|
|
7
|
+
* knowledge.jsonl entry to be silently dropped on load. Treat BOM as a
|
|
8
|
+
* no-op at read time so the rest of the pipeline sees clean UTF-8.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stripBom(text: string): string;
|
|
2
11
|
export interface DirectoryLockOptions {
|
|
3
12
|
retries?: number;
|
|
4
13
|
retryDelayMs?: number;
|
package/dist/fs-utils.js
CHANGED
|
@@ -3,6 +3,17 @@ import path from "node:path";
|
|
|
3
3
|
export async function ensureDir(dirPath) {
|
|
4
4
|
await fs.mkdir(dirPath, { recursive: true });
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Strip a leading UTF-8 BOM (U+FEFF) if present. Many editors (VS Code on
|
|
8
|
+
* Windows, Notepad, some CI tools) silently prepend a BOM when saving
|
|
9
|
+
* UTF-8; when the file is then split on `\n` the first line keeps the
|
|
10
|
+
* invisible BOM and `JSON.parse` rejects it, which caused the first
|
|
11
|
+
* knowledge.jsonl entry to be silently dropped on load. Treat BOM as a
|
|
12
|
+
* no-op at read time so the rest of the pipeline sees clean UTF-8.
|
|
13
|
+
*/
|
|
14
|
+
export function stripBom(text) {
|
|
15
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
16
|
+
}
|
|
6
17
|
function sleep(ms) {
|
|
7
18
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
19
|
}
|
|
@@ -54,7 +65,30 @@ export async function writeFileSafe(filePath, content) {
|
|
|
54
65
|
await ensureDir(path.dirname(filePath));
|
|
55
66
|
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
56
67
|
await fs.writeFile(tempPath, content, "utf8");
|
|
57
|
-
|
|
68
|
+
try {
|
|
69
|
+
await fs.rename(tempPath, filePath);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const code = error?.code;
|
|
73
|
+
// `rename` fails with EXDEV when the temp file and target live on
|
|
74
|
+
// different filesystems (container bind mounts, tmpfs + rootfs,
|
|
75
|
+
// cross-volume setups). Fall back to copy + unlink so atomic writes
|
|
76
|
+
// still work — copyFile is not fully atomic but is the best we can
|
|
77
|
+
// do across devices, and we remove the temp even if copy fails.
|
|
78
|
+
if (code === "EXDEV") {
|
|
79
|
+
try {
|
|
80
|
+
await fs.copyFile(tempPath, filePath);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Other errors: try to clean up the temp to avoid littering the
|
|
88
|
+
// directory with orphaned `.tmp-<pid>-*` files, then rethrow.
|
|
89
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
58
92
|
}
|
|
59
93
|
export async function exists(filePath) {
|
|
60
94
|
try {
|
package/dist/gate-evidence.js
CHANGED
|
@@ -212,7 +212,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
212
212
|
const artifactPresent = await currentStageArtifactExists(projectRoot, stage, flowState.track);
|
|
213
213
|
const shouldValidateArtifact = artifactPresent || catalog.passed.length > 0 || flowState.completedStages.includes(stage);
|
|
214
214
|
if (shouldValidateArtifact) {
|
|
215
|
-
const lint = await lintArtifact(projectRoot, stage);
|
|
215
|
+
const lint = await lintArtifact(projectRoot, stage, flowState.track);
|
|
216
216
|
if (!lint.passed) {
|
|
217
217
|
const failedRequired = lint.findings
|
|
218
218
|
.filter((finding) => finding.required && !finding.found)
|
package/dist/gitignore.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { REQUIRED_GITIGNORE_PATTERNS } from "./constants.js";
|
|
4
|
-
import { exists } from "./fs-utils.js";
|
|
4
|
+
import { exists, writeFileSafe } from "./fs-utils.js";
|
|
5
5
|
export async function ensureGitignore(projectRoot) {
|
|
6
6
|
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
7
7
|
const currentContent = (await exists(gitignorePath))
|
|
@@ -15,7 +15,10 @@ export async function ensureGitignore(projectRoot) {
|
|
|
15
15
|
}
|
|
16
16
|
const base = lines.join("\n").replace(/\s+$/u, "");
|
|
17
17
|
const suffix = `${base.length > 0 ? "\n" : ""}${missing.join("\n")}\n`;
|
|
18
|
-
|
|
18
|
+
// `writeFileSafe` performs a tmp-file + rename so a crash mid-write
|
|
19
|
+
// cannot leave `.gitignore` in a half-written state; the previous
|
|
20
|
+
// direct `fs.writeFile` could truncate the file on SIGKILL.
|
|
21
|
+
await writeFileSafe(gitignorePath, `${base}${suffix}`);
|
|
19
22
|
}
|
|
20
23
|
export async function removeGitignorePatterns(projectRoot) {
|
|
21
24
|
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
@@ -30,7 +33,7 @@ export async function removeGitignorePatterns(projectRoot) {
|
|
|
30
33
|
await fs.rm(gitignorePath, { force: true });
|
|
31
34
|
}
|
|
32
35
|
else {
|
|
33
|
-
await
|
|
36
|
+
await writeFileSafe(gitignorePath, `${result}\n`);
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
export async function gitignoreHasRequiredPatterns(projectRoot) {
|
package/dist/harness-adapters.js
CHANGED
|
@@ -54,7 +54,12 @@ const LEGACY_CODEX_SKILL_NAMES = [
|
|
|
54
54
|
"cclaw-cc-next",
|
|
55
55
|
"cclaw-cc-view",
|
|
56
56
|
"cclaw-cc-ops",
|
|
57
|
-
"cclaw-cc-ideate"
|
|
57
|
+
"cclaw-cc-ideate",
|
|
58
|
+
// Pre-v0.40 installed `/cc-learn` as a top-level skill before it was
|
|
59
|
+
// folded into `/cc-ops`. Without this entry the orphan stays behind
|
|
60
|
+
// after upgrade and Codex lists both the new in-thread workflow and
|
|
61
|
+
// the legacy slash command.
|
|
62
|
+
"cclaw-cc-learn"
|
|
58
63
|
];
|
|
59
64
|
/**
|
|
60
65
|
* Shims that older cclaw versions installed as top-level slash commands but
|
|
@@ -417,6 +422,11 @@ what the hook surface does and does not cover.
|
|
|
417
422
|
are **not** gated by hooks — read
|
|
418
423
|
\`.cclaw/references/harnesses/codex-playbook.md\` for what cclaw
|
|
419
424
|
substitutes with in-turn agent steps for those call classes.
|
|
425
|
+
- Codex's \`SessionStart\` matcher only supports \`startup|resume\`. Claude
|
|
426
|
+
and Cursor also fire on \`clear\` and \`compact\`, so mid-session
|
|
427
|
+
context resets there re-inject cclaw's bootstrap automatically. In
|
|
428
|
+
Codex you must re-announce the active stage yourself after any
|
|
429
|
+
\`/clear\` or compaction — the skill does not reload implicitly.
|
|
420
430
|
`;
|
|
421
431
|
}
|
|
422
432
|
function codexSkillMarkdown(command, skillName, skillFolder, commandFile) {
|
package/dist/install.js
CHANGED
|
@@ -44,7 +44,8 @@ import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
|
44
44
|
import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
45
45
|
import { HARNESS_ADAPTERS, harnessShimFileNames, harnessTier, syncHarnessShims, removeCclawFromAgentsMd } from "./harness-adapters.js";
|
|
46
46
|
import { validateHookDocument } from "./hook-schema.js";
|
|
47
|
-
import {
|
|
47
|
+
import { detectHarnesses } from "./init-detect.js";
|
|
48
|
+
import { CorruptFlowStateError, ensureRunSystem, readFlowState } from "./runs.js";
|
|
48
49
|
import { FLOW_STAGES } from "./types.js";
|
|
49
50
|
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
50
51
|
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
@@ -853,7 +854,24 @@ Drop this section if no hard rule applies. Keep it crisp:
|
|
|
853
854
|
async function ensureSessionStateFiles(projectRoot) {
|
|
854
855
|
const stateDir = runtimePath(projectRoot, "state");
|
|
855
856
|
await ensureDir(stateDir);
|
|
856
|
-
|
|
857
|
+
// If flow-state.json is corrupt, `readFlowState` quarantines the bad
|
|
858
|
+
// file and throws. During install we'd rather continue than abort:
|
|
859
|
+
// the user just asked to set up cclaw, and the corrupt file is already
|
|
860
|
+
// preserved next to the original path. Fall back to a fresh initial
|
|
861
|
+
// state so the rest of install completes and the user can inspect the
|
|
862
|
+
// `.corrupt-<timestamp>.json` quarantine afterwards.
|
|
863
|
+
let flow;
|
|
864
|
+
try {
|
|
865
|
+
flow = await readFlowState(projectRoot);
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
if (err instanceof CorruptFlowStateError) {
|
|
869
|
+
flow = createInitialFlowState();
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
throw err;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
857
875
|
const activityPath = path.join(stateDir, "stage-activity.jsonl");
|
|
858
876
|
if (!(await exists(activityPath))) {
|
|
859
877
|
await writeFileSafe(activityPath, "");
|
|
@@ -1021,6 +1039,14 @@ async function writeHarnessGapsState(projectRoot, harnesses) {
|
|
|
1021
1039
|
break;
|
|
1022
1040
|
}
|
|
1023
1041
|
for (const event of missingHookEvents) {
|
|
1042
|
+
if (harness === "codex" && event === "precompact_digest") {
|
|
1043
|
+
// Codex CLI has no PreCompact event. Generic "schedule the script
|
|
1044
|
+
// manually" copy doesn't help; instead, point the agent at the
|
|
1045
|
+
// in-thread substitute that already exists in cclaw content
|
|
1046
|
+
// (`/cc-ops retro` reads the same digest the hook would emit).
|
|
1047
|
+
remediation.push("hook event precompact_digest → Codex has no PreCompact event; run `/cc-ops retro` in-thread before compaction instead of relying on a hook");
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1024
1050
|
remediation.push(`hook event ${event} → schedule the corresponding script manually or accept reduced observability`);
|
|
1025
1051
|
}
|
|
1026
1052
|
return {
|
|
@@ -1210,9 +1236,19 @@ export async function initCclaw(options) {
|
|
|
1210
1236
|
}
|
|
1211
1237
|
export async function syncCclaw(projectRoot) {
|
|
1212
1238
|
const configExists = await exists(configPath(projectRoot));
|
|
1213
|
-
|
|
1239
|
+
let config = await readConfig(projectRoot);
|
|
1214
1240
|
if (!configExists) {
|
|
1215
|
-
|
|
1241
|
+
// Prefer detected harness markers over the hardcoded default list.
|
|
1242
|
+
// Without this, a user running `cclaw sync` in a `.claude`-only
|
|
1243
|
+
// project ends up with a config that also enables cursor/opencode/
|
|
1244
|
+
// codex, which then fails doctor checks for missing shim folders.
|
|
1245
|
+
// Fall back to the previous default (config.harnesses) if no markers
|
|
1246
|
+
// are found so brand-new projects still bootstrap cleanly.
|
|
1247
|
+
const detected = await detectHarnesses(projectRoot);
|
|
1248
|
+
const harnesses = detected.length > 0 ? detected : config.harnesses;
|
|
1249
|
+
const defaultConfig = createDefaultConfig(harnesses);
|
|
1250
|
+
await writeConfig(projectRoot, defaultConfig);
|
|
1251
|
+
config = defaultConfig;
|
|
1216
1252
|
}
|
|
1217
1253
|
await materializeRuntime(projectRoot, config, false);
|
|
1218
1254
|
}
|
package/dist/knowledge-store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
|
-
import { withDirectoryLock } from "./fs-utils.js";
|
|
4
|
+
import { stripBom, withDirectoryLock } from "./fs-utils.js";
|
|
5
5
|
import { FLOW_STAGES } from "./types.js";
|
|
6
6
|
const KNOWLEDGE_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
|
|
7
7
|
const KNOWLEDGE_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
|
|
@@ -179,7 +179,7 @@ export function materializeKnowledgeEntry(seed, defaults = {}) {
|
|
|
179
179
|
async function readExistingKnowledgeKeys(filePath) {
|
|
180
180
|
const keys = new Set();
|
|
181
181
|
try {
|
|
182
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
182
|
+
const raw = stripBom(await fs.readFile(filePath, "utf8"));
|
|
183
183
|
const lines = raw.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
184
184
|
for (const line of lines) {
|
|
185
185
|
try {
|
package/dist/retro-gate.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
|
-
import { exists } from "./fs-utils.js";
|
|
4
|
+
import { exists, stripBom } from "./fs-utils.js";
|
|
5
5
|
function activeArtifactsPath(projectRoot) {
|
|
6
6
|
return path.join(projectRoot, RUNTIME_ROOT, "artifacts");
|
|
7
7
|
}
|
|
8
8
|
function retroArtifactPath(projectRoot) {
|
|
9
9
|
return path.join(activeArtifactsPath(projectRoot), "09-retro.md");
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
// Fallback window for compound-entry scanning when `retroDraftedAt` /
|
|
12
|
+
// `retroAcceptedAt` are not set (legacy runs or imports): use the retro
|
|
13
|
+
// artifact's mtime ± 7 days. 24h was too narrow for long-running retros
|
|
14
|
+
// that are edited over several days or runs imported from another
|
|
15
|
+
// machine with slightly different clocks; 7 days is still tight enough
|
|
16
|
+
// that entries from an unrelated future run are excluded.
|
|
17
|
+
const RETRO_ARTIFACT_MTIME_FALLBACK_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
12
18
|
function parseIsoTimestamp(value) {
|
|
13
19
|
if (!value || value.trim().length === 0)
|
|
14
20
|
return null;
|
|
@@ -58,7 +64,7 @@ export async function evaluateRetroGate(projectRoot, state) {
|
|
|
58
64
|
const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|
|
59
65
|
if (shouldFallbackScan && (await exists(knowledgeFile))) {
|
|
60
66
|
try {
|
|
61
|
-
const raw = await fs.readFile(knowledgeFile, "utf8");
|
|
67
|
+
const raw = stripBom(await fs.readFile(knowledgeFile, "utf8"));
|
|
62
68
|
compoundEntries = 0;
|
|
63
69
|
for (const line of raw.split(/\r?\n/)) {
|
|
64
70
|
const trimmed = line.trim();
|
|
@@ -90,17 +96,20 @@ export async function evaluateRetroGate(projectRoot, state) {
|
|
|
90
96
|
compoundEntries = 0;
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
|
-
// A retro is considered complete when
|
|
94
|
-
// - at least one compound learning was
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
// A retro is considered complete when any of:
|
|
100
|
+
// - the retro artifact exists AND (at least one compound learning was
|
|
101
|
+
// promoted during the retro window OR compound was explicitly skipped
|
|
102
|
+
// after reviewing the draft), or
|
|
103
|
+
// - the operator explicitly skipped the retro step itself
|
|
104
|
+
// (`retroSkipped === true` with a reason). `retroSkipped` is an
|
|
105
|
+
// operator-level override of the artifact requirement, so it must
|
|
106
|
+
// bypass `hasRetroArtifact` — otherwise a run that legitimately had
|
|
107
|
+
// nothing worth retro-ing dead-locks at closeout waiting for a
|
|
108
|
+
// file that will never exist.
|
|
109
|
+
const retroSkipped = state.closeout.retroSkipped === true;
|
|
110
|
+
const compoundSkipped = state.closeout.compoundSkipped === true;
|
|
111
|
+
const artifactPathComplete = hasRetroArtifact && (compoundEntries > 0 || compoundSkipped);
|
|
112
|
+
const completed = required ? retroSkipped || artifactPathComplete : true;
|
|
104
113
|
return {
|
|
105
114
|
required,
|
|
106
115
|
completed,
|
package/dist/run-archive.js
CHANGED
|
@@ -204,40 +204,70 @@ export async function archiveRun(projectRoot, featureName, options = {}) {
|
|
|
204
204
|
compoundEntries: retroGate.compoundEntries
|
|
205
205
|
};
|
|
206
206
|
await ensureDir(archivePath);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await resetCarryoverStateFiles(projectRoot, resetState.activeRunId);
|
|
207
|
+
// Drop an `.archive-in-progress` sentinel immediately so that a crash
|
|
208
|
+
// between the artifact rename and the final manifest write leaves a
|
|
209
|
+
// recoverable marker (doctor surfaces these; re-running archive on an
|
|
210
|
+
// orphan attempts to complete or roll back). The sentinel is removed
|
|
211
|
+
// only after the manifest lands successfully.
|
|
212
|
+
const sentinelPath = path.join(archivePath, ".archive-in-progress");
|
|
214
213
|
const archivedAt = new Date().toISOString();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
214
|
+
await writeFileSafe(sentinelPath, `${JSON.stringify({ archiveId, startedAt: archivedAt, sourceRunId: sourceState.activeRunId }, null, 2)}\n`);
|
|
215
|
+
let artifactsMoved = false;
|
|
216
|
+
try {
|
|
217
|
+
await fs.rename(artifactsDir, archiveArtifactsPath);
|
|
218
|
+
artifactsMoved = true;
|
|
219
|
+
await ensureDir(artifactsDir);
|
|
220
|
+
const archiveStatePath = path.join(archivePath, "state");
|
|
221
|
+
const snapshottedStateFiles = await snapshotStateDirectory(projectRoot, archiveStatePath);
|
|
222
|
+
const resetState = createInitialFlowState();
|
|
223
|
+
await writeFlowState(projectRoot, resetState, { allowReset: true });
|
|
224
|
+
await resetCarryoverStateFiles(projectRoot, resetState.activeRunId);
|
|
225
|
+
const manifest = {
|
|
226
|
+
version: 1,
|
|
227
|
+
archiveId,
|
|
228
|
+
archivedAt,
|
|
229
|
+
featureName: feature,
|
|
230
|
+
activeFeature,
|
|
231
|
+
sourceRunId: sourceState.activeRunId,
|
|
232
|
+
sourceCurrentStage: sourceState.currentStage,
|
|
233
|
+
sourceCompletedStages: sourceState.completedStages,
|
|
234
|
+
snapshottedStateFiles,
|
|
235
|
+
retro: retroSummary
|
|
236
|
+
};
|
|
237
|
+
await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
238
|
+
// Manifest landed — sentinel is no longer needed.
|
|
239
|
+
await fs.unlink(sentinelPath).catch(() => undefined);
|
|
240
|
+
const knowledgeStats = await readKnowledgeStats(projectRoot);
|
|
241
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
242
|
+
return {
|
|
243
|
+
archiveId,
|
|
244
|
+
archivePath,
|
|
245
|
+
archivedAt,
|
|
246
|
+
featureName: feature,
|
|
247
|
+
activeFeature,
|
|
248
|
+
resetState,
|
|
249
|
+
snapshottedStateFiles,
|
|
250
|
+
knowledge: knowledgeStats,
|
|
251
|
+
retro: retroSummary
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
// Best-effort rollback: if artifacts were moved but the subsequent
|
|
256
|
+
// steps failed, put artifacts back so the user is not left without
|
|
257
|
+
// a working run. The sentinel is intentionally left behind for
|
|
258
|
+
// inspection; doctor surfaces it.
|
|
259
|
+
if (artifactsMoved) {
|
|
260
|
+
try {
|
|
261
|
+
await fs.rm(artifactsDir, { recursive: true, force: true });
|
|
262
|
+
await fs.rename(archiveArtifactsPath, artifactsDir);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Rollback failed — sentinel + orphaned archive dir will be
|
|
266
|
+
// surfaced by doctor and can be reconciled manually.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
241
271
|
}
|
|
242
272
|
const KNOWLEDGE_SOFT_THRESHOLD = 50;
|
|
243
273
|
async function readKnowledgeStats(projectRoot) {
|
package/dist/run-persistence.js
CHANGED
|
@@ -24,6 +24,13 @@ function validateFlowTransition(prev, next) {
|
|
|
24
24
|
// New run — only reset paths may change the runId, but those set allowReset.
|
|
25
25
|
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change activeRunId from "${prev.activeRunId}" to "${next.activeRunId}" without allowReset.`);
|
|
26
26
|
}
|
|
27
|
+
// Track is immutable within a single run: stage schemas, gate sets, and
|
|
28
|
+
// cross-stage reads all branch on track. Silently flipping the track
|
|
29
|
+
// mid-run would let completed stages satisfy one gate tier and the
|
|
30
|
+
// current stage re-read the catalog under a different tier.
|
|
31
|
+
if (prev.track !== next.track) {
|
|
32
|
+
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change track from "${prev.track}" to "${next.track}" mid-run (activeRunId="${prev.activeRunId}"). Archive the run and start a new one to switch tracks.`);
|
|
33
|
+
}
|
|
27
34
|
for (const completed of prev.completedStages) {
|
|
28
35
|
if (!next.completedStages.includes(completed)) {
|
|
29
36
|
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `completedStages must be monotonic: stage "${completed}" was previously completed but is missing from the new state.`);
|
package/dist/tdd-cycle.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export function parseTddCycleLog(text) {
|
|
2
2
|
const out = [];
|
|
3
|
-
|
|
3
|
+
// Strip a leading UTF-8 BOM on the whole blob so the first line parses
|
|
4
|
+
// cleanly; `trim()` handles BOM on subsequent lines through the same
|
|
5
|
+
// codepath (empty/whitespace-only lines are skipped).
|
|
6
|
+
const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
7
|
+
for (const raw of normalized.split(/\r?\n/)) {
|
|
4
8
|
const line = raw.trim();
|
|
5
9
|
if (!line)
|
|
6
10
|
continue;
|
|
@@ -100,6 +104,7 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
100
104
|
}
|
|
101
105
|
if (state !== "green_done") {
|
|
102
106
|
issues.push(`slice ${slice}: refactor logged before green`);
|
|
107
|
+
continue;
|
|
103
108
|
}
|
|
104
109
|
state = "need_red";
|
|
105
110
|
}
|