cclaw-cli 0.55.2 → 1.0.0
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 +3 -3
- package/dist/artifact-linter/brainstorm.js +45 -1
- package/dist/artifact-linter/design.js +32 -1
- package/dist/artifact-linter/plan.js +22 -1
- package/dist/artifact-linter/review.js +35 -1
- package/dist/artifact-linter/scope.js +19 -9
- package/dist/artifact-linter/shared.d.ts +11 -10
- package/dist/artifact-linter/shared.js +70 -41
- package/dist/artifact-linter/ship.js +36 -0
- package/dist/artifact-linter/spec.js +23 -1
- package/dist/artifact-linter/tdd.js +74 -0
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -0
- package/dist/content/closeout-guidance.d.ts +1 -1
- package/dist/content/closeout-guidance.js +10 -11
- package/dist/content/core-agents.d.ts +35 -36
- package/dist/content/core-agents.js +189 -99
- package/dist/content/diff-command.js +1 -1
- package/dist/content/examples.d.ts +0 -3
- package/dist/content/examples.js +197 -752
- package/dist/content/idea.d.ts +60 -0
- package/dist/content/idea.js +404 -0
- package/dist/content/learnings.d.ts +2 -4
- package/dist/content/learnings.js +10 -26
- package/dist/content/node-hooks.js +131 -97
- package/dist/content/opencode-plugin.js +12 -26
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/runtime-shared-snippets.d.ts +8 -0
- package/dist/content/runtime-shared-snippets.js +80 -0
- package/dist/content/session-hooks.js +1 -1
- package/dist/content/skills.d.ts +1 -0
- package/dist/content/skills.js +50 -0
- package/dist/content/stage-schema.js +107 -63
- package/dist/content/stages/review.js +8 -8
- package/dist/content/stages/schema-types.d.ts +2 -2
- package/dist/content/stages/scope.js +1 -1
- package/dist/content/stages/ship.js +1 -1
- package/dist/content/status-command.js +3 -3
- package/dist/content/subagent-context-skills.js +156 -1
- package/dist/content/subagents.d.ts +0 -5
- package/dist/content/subagents.js +12 -82
- package/dist/content/templates.js +87 -6
- package/dist/content/utility-skills.js +26 -97
- package/dist/flow-state.d.ts +5 -6
- package/dist/flow-state.js +4 -6
- package/dist/gate-evidence.d.ts +0 -31
- package/dist/gate-evidence.js +3 -181
- package/dist/harness-adapters.js +1 -1
- package/dist/install.js +38 -4
- package/dist/internal/advance-stage/advance.js +0 -1
- package/dist/internal/advance-stage/review-loop.js +1 -10
- package/dist/knowledge-store.d.ts +2 -20
- package/dist/knowledge-store.js +43 -57
- package/dist/policy.js +3 -3
- package/dist/retro-gate.js +8 -90
- package/dist/run-archive.js +1 -4
- package/dist/run-persistence.js +14 -109
- package/dist/runtime/run-hook.entry.d.ts +3 -0
- package/dist/runtime/run-hook.entry.js +5 -0
- package/dist/runtime/run-hook.mjs +9477 -0
- package/package.json +4 -2
- package/dist/content/hook-inline-snippets.d.ts +0 -96
- package/dist/content/hook-inline-snippets.js +0 -515
- package/dist/content/idea-command.d.ts +0 -8
- package/dist/content/idea-command.js +0 -322
- package/dist/content/idea-frames.d.ts +0 -31
- package/dist/content/idea-frames.js +0 -140
- package/dist/content/idea-ranking.d.ts +0 -25
- package/dist/content/idea-ranking.js +0 -65
- package/dist/trace-matrix.d.ts +0 -27
- package/dist/trace-matrix.js +0 -226
package/dist/knowledge-store.js
CHANGED
|
@@ -32,9 +32,8 @@ export function effectiveCompoundThreshold(baseThreshold, archivedRunsCount) {
|
|
|
32
32
|
*
|
|
33
33
|
* Clustering key: `(type, normalizeText(trigger), normalizeText(action))`
|
|
34
34
|
* which mirrors the compound readiness clustering in runtime state.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* as ready.
|
|
35
|
+
* The readiness surface intentionally stays simple: no maturity/supersede
|
|
36
|
+
* suppression logic in this pass.
|
|
38
37
|
*/
|
|
39
38
|
export function computeCompoundReadiness(entries, options = {}) {
|
|
40
39
|
const thresholdRaw = options.threshold ?? DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
@@ -54,8 +53,6 @@ export function computeCompoundReadiness(entries, options = {}) {
|
|
|
54
53
|
const { threshold, relaxationApplied } = effectiveCompoundThreshold(baseThreshold, archivedRunsCount);
|
|
55
54
|
const buckets = new Map();
|
|
56
55
|
for (const entry of entries) {
|
|
57
|
-
if (entry.maturity === "lifted-to-enforcement" || entry.superseded_by !== undefined)
|
|
58
|
-
continue;
|
|
59
56
|
const key = [
|
|
60
57
|
entry.type,
|
|
61
58
|
normalizeText(entry.trigger),
|
|
@@ -71,15 +68,13 @@ export function computeCompoundReadiness(entries, options = {}) {
|
|
|
71
68
|
entryCount: 1,
|
|
72
69
|
severity: entry.severity,
|
|
73
70
|
lastSeenTs: entry.last_seen_ts,
|
|
74
|
-
types: new Set([entry.type])
|
|
75
|
-
maturity: new Set([entry.maturity])
|
|
71
|
+
types: new Set([entry.type])
|
|
76
72
|
});
|
|
77
73
|
continue;
|
|
78
74
|
}
|
|
79
75
|
bucket.recurrence += frequency;
|
|
80
76
|
bucket.entryCount += 1;
|
|
81
77
|
bucket.types.add(entry.type);
|
|
82
|
-
bucket.maturity.add(entry.maturity);
|
|
83
78
|
if (entry.severity === "critical") {
|
|
84
79
|
bucket.severity = "critical";
|
|
85
80
|
}
|
|
@@ -104,8 +99,7 @@ export function computeCompoundReadiness(entries, options = {}) {
|
|
|
104
99
|
qualification: criticalOverride && !meetsRecurrence ? "critical_override" : "recurrence",
|
|
105
100
|
...(bucket.severity ? { severity: bucket.severity } : {}),
|
|
106
101
|
lastSeenTs: bucket.lastSeenTs,
|
|
107
|
-
types: Array.from(bucket.types).sort()
|
|
108
|
-
maturity: Array.from(bucket.maturity).sort()
|
|
102
|
+
types: Array.from(bucket.types).sort()
|
|
109
103
|
});
|
|
110
104
|
}
|
|
111
105
|
ready.sort((a, b) => {
|
|
@@ -143,8 +137,8 @@ export function computeCompoundReadiness(entries, options = {}) {
|
|
|
143
137
|
const KNOWLEDGE_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
|
|
144
138
|
const KNOWLEDGE_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
|
|
145
139
|
const KNOWLEDGE_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
|
|
146
|
-
const
|
|
147
|
-
const
|
|
140
|
+
const LEGACY_KNOWLEDGE_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
|
|
141
|
+
const LEGACY_KNOWLEDGE_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
|
|
148
142
|
const KNOWLEDGE_SOURCE_SET = new Set([
|
|
149
143
|
"stage",
|
|
150
144
|
"retro",
|
|
@@ -158,19 +152,21 @@ const KNOWLEDGE_REQUIRED_KEYS = [
|
|
|
158
152
|
"trigger",
|
|
159
153
|
"action",
|
|
160
154
|
"confidence",
|
|
161
|
-
"domain",
|
|
162
155
|
"stage",
|
|
163
156
|
"origin_stage",
|
|
164
|
-
"origin_run",
|
|
165
157
|
"frequency",
|
|
166
|
-
"universality",
|
|
167
|
-
"maturity",
|
|
168
158
|
"created",
|
|
169
159
|
"first_seen_ts",
|
|
170
160
|
"last_seen_ts",
|
|
171
161
|
"project"
|
|
172
162
|
];
|
|
173
163
|
const KNOWLEDGE_ALLOWED_KEYS = new Set(KNOWLEDGE_REQUIRED_KEYS);
|
|
164
|
+
// Legacy keys are accepted for backwards compatibility when reading historical
|
|
165
|
+
// knowledge entries, but no longer required for newly materialized rows.
|
|
166
|
+
KNOWLEDGE_ALLOWED_KEYS.add("domain");
|
|
167
|
+
KNOWLEDGE_ALLOWED_KEYS.add("origin_run");
|
|
168
|
+
KNOWLEDGE_ALLOWED_KEYS.add("universality");
|
|
169
|
+
KNOWLEDGE_ALLOWED_KEYS.add("maturity");
|
|
174
170
|
KNOWLEDGE_ALLOWED_KEYS.add("source");
|
|
175
171
|
KNOWLEDGE_ALLOWED_KEYS.add("severity");
|
|
176
172
|
KNOWLEDGE_ALLOWED_KEYS.add("supersedes");
|
|
@@ -178,6 +174,28 @@ KNOWLEDGE_ALLOWED_KEYS.add("superseded_by");
|
|
|
178
174
|
function keyAllowedInKnowledgeEntry(key) {
|
|
179
175
|
return KNOWLEDGE_ALLOWED_KEYS.has(key);
|
|
180
176
|
}
|
|
177
|
+
function normalizeParsedKnowledgeEntry(obj) {
|
|
178
|
+
const normalized = {
|
|
179
|
+
type: obj.type,
|
|
180
|
+
trigger: obj.trigger,
|
|
181
|
+
action: obj.action,
|
|
182
|
+
confidence: obj.confidence,
|
|
183
|
+
stage: obj.stage,
|
|
184
|
+
origin_stage: obj.origin_stage,
|
|
185
|
+
frequency: obj.frequency,
|
|
186
|
+
created: obj.created,
|
|
187
|
+
first_seen_ts: obj.first_seen_ts,
|
|
188
|
+
last_seen_ts: obj.last_seen_ts,
|
|
189
|
+
project: obj.project
|
|
190
|
+
};
|
|
191
|
+
if (obj.severity !== undefined) {
|
|
192
|
+
normalized.severity = obj.severity;
|
|
193
|
+
}
|
|
194
|
+
if (obj.source !== undefined) {
|
|
195
|
+
normalized.source = obj.source;
|
|
196
|
+
}
|
|
197
|
+
return normalized;
|
|
198
|
+
}
|
|
181
199
|
function knowledgePath(projectRoot) {
|
|
182
200
|
return path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|
|
183
201
|
}
|
|
@@ -198,16 +216,11 @@ function dedupeKey(entry) {
|
|
|
198
216
|
entry.type,
|
|
199
217
|
normalizeText(entry.trigger),
|
|
200
218
|
normalizeText(entry.action),
|
|
201
|
-
entry.domain === null ? "null" : normalizeText(entry.domain),
|
|
202
219
|
entry.stage ?? "null",
|
|
203
220
|
entry.origin_stage ?? "null",
|
|
204
|
-
entry.origin_run === null ? "null" : normalizeText(entry.origin_run),
|
|
205
|
-
entry.universality,
|
|
206
221
|
entry.project === null ? "null" : normalizeText(entry.project),
|
|
207
222
|
entry.source === undefined || entry.source === null ? "null" : entry.source,
|
|
208
|
-
entry.severity === undefined ? "none" : entry.severity
|
|
209
|
-
Array.isArray(entry.supersedes) ? entry.supersedes.map(normalizeText).sort().join(",") : "none",
|
|
210
|
-
entry.superseded_by === undefined ? "none" : normalizeText(entry.superseded_by)
|
|
223
|
+
entry.severity === undefined ? "none" : entry.severity
|
|
211
224
|
].join("|");
|
|
212
225
|
}
|
|
213
226
|
function emptyKnowledgeSnapshot() {
|
|
@@ -236,7 +249,7 @@ function parseKnowledgeSnapshot(raw) {
|
|
|
236
249
|
malformedLines += 1;
|
|
237
250
|
continue;
|
|
238
251
|
}
|
|
239
|
-
const entry = parsed;
|
|
252
|
+
const entry = normalizeParsedKnowledgeEntry(parsed);
|
|
240
253
|
entries.push(entry);
|
|
241
254
|
const key = dedupeKey(entry);
|
|
242
255
|
if (!keyToIndex.has(key)) {
|
|
@@ -321,7 +334,7 @@ export function validateKnowledgeEntry(entry) {
|
|
|
321
334
|
(typeof obj.severity !== "string" || !KNOWLEDGE_SEVERITY_SET.has(obj.severity))) {
|
|
322
335
|
errors.push("severity must be one of: critical, important, suggestion.");
|
|
323
336
|
}
|
|
324
|
-
if (!isNullableString(obj.domain)) {
|
|
337
|
+
if (obj.domain !== undefined && !isNullableString(obj.domain)) {
|
|
325
338
|
errors.push("domain must be string or null.");
|
|
326
339
|
}
|
|
327
340
|
if (!isNullableStage(obj.stage)) {
|
|
@@ -331,7 +344,7 @@ export function validateKnowledgeEntry(entry) {
|
|
|
331
344
|
errors.push(`origin_stage must be one of ${FLOW_STAGES.join(", ")} or null.`);
|
|
332
345
|
}
|
|
333
346
|
const originRun = obj.origin_run;
|
|
334
|
-
if (!isNullableString(originRun)) {
|
|
347
|
+
if (originRun !== undefined && !isNullableString(originRun)) {
|
|
335
348
|
errors.push("origin_run must be string or null.");
|
|
336
349
|
}
|
|
337
350
|
if (typeof obj.frequency !== "number" ||
|
|
@@ -339,10 +352,12 @@ export function validateKnowledgeEntry(entry) {
|
|
|
339
352
|
obj.frequency < 1) {
|
|
340
353
|
errors.push("frequency must be an integer >= 1.");
|
|
341
354
|
}
|
|
342
|
-
if (
|
|
355
|
+
if (obj.universality !== undefined &&
|
|
356
|
+
(typeof obj.universality !== "string" || !LEGACY_KNOWLEDGE_UNIVERSALITY_SET.has(obj.universality))) {
|
|
343
357
|
errors.push("universality must be one of: project, personal, universal.");
|
|
344
358
|
}
|
|
345
|
-
if (
|
|
359
|
+
if (obj.maturity !== undefined &&
|
|
360
|
+
(typeof obj.maturity !== "string" || !LEGACY_KNOWLEDGE_MATURITY_SET.has(obj.maturity))) {
|
|
346
361
|
errors.push("maturity must be one of: raw, lifted-to-rule, lifted-to-enforcement.");
|
|
347
362
|
}
|
|
348
363
|
for (const timestampField of ["created", "first_seen_ts", "last_seen_ts"]) {
|
|
@@ -378,20 +393,15 @@ export function materializeKnowledgeEntry(seed, defaults = {}) {
|
|
|
378
393
|
const now = normalizeUtcIso(defaults.nowIso ?? nowUtcIso());
|
|
379
394
|
const stage = seed.stage ?? defaults.stage ?? null;
|
|
380
395
|
const originStage = seed.origin_stage ?? defaults.originStage ?? stage ?? null;
|
|
381
|
-
const originRun = seed.origin_run ?? defaults.originRun ?? null;
|
|
382
396
|
const source = seed.source ?? defaults.source ?? null;
|
|
383
397
|
const entry = {
|
|
384
398
|
type: seed.type,
|
|
385
399
|
trigger: seed.trigger.trim(),
|
|
386
400
|
action: seed.action.trim(),
|
|
387
401
|
confidence: seed.confidence,
|
|
388
|
-
domain: seed.domain ?? null,
|
|
389
402
|
stage,
|
|
390
403
|
origin_stage: originStage,
|
|
391
|
-
origin_run: originRun,
|
|
392
404
|
frequency: seed.frequency ?? 1,
|
|
393
|
-
universality: seed.universality ?? "project",
|
|
394
|
-
maturity: seed.maturity ?? "raw",
|
|
395
405
|
created: normalizeUtcIso(seed.created ?? now),
|
|
396
406
|
first_seen_ts: normalizeUtcIso(seed.first_seen_ts ?? now),
|
|
397
407
|
last_seen_ts: normalizeUtcIso(seed.last_seen_ts ?? now),
|
|
@@ -400,12 +410,6 @@ export function materializeKnowledgeEntry(seed, defaults = {}) {
|
|
|
400
410
|
if (seed.severity !== undefined) {
|
|
401
411
|
entry.severity = seed.severity;
|
|
402
412
|
}
|
|
403
|
-
if (seed.supersedes !== undefined) {
|
|
404
|
-
entry.supersedes = seed.supersedes.map((value) => value.trim());
|
|
405
|
-
}
|
|
406
|
-
if (seed.superseded_by !== undefined) {
|
|
407
|
-
entry.superseded_by = seed.superseded_by.trim();
|
|
408
|
-
}
|
|
409
413
|
if (source !== null) {
|
|
410
414
|
entry.source = source;
|
|
411
415
|
}
|
|
@@ -514,18 +518,6 @@ function tokenizeText(value) {
|
|
|
514
518
|
function uniqueTokens(values) {
|
|
515
519
|
return [...new Set(values)];
|
|
516
520
|
}
|
|
517
|
-
function supersededTriggerSet(entries) {
|
|
518
|
-
const superseded = new Set();
|
|
519
|
-
for (const entry of entries) {
|
|
520
|
-
for (const trigger of entry.supersedes ?? []) {
|
|
521
|
-
superseded.add(normalizeText(trigger));
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
return superseded;
|
|
525
|
-
}
|
|
526
|
-
function isSupersededLearning(entry, supersededTriggers) {
|
|
527
|
-
return entry.superseded_by !== undefined || supersededTriggers.has(normalizeText(entry.trigger));
|
|
528
|
-
}
|
|
529
521
|
function pathTokens(paths) {
|
|
530
522
|
if (!Array.isArray(paths) || paths.length === 0)
|
|
531
523
|
return [];
|
|
@@ -547,9 +539,7 @@ export async function selectRelevantLearnings(projectRoot, options = {}) {
|
|
|
547
539
|
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0
|
|
548
540
|
? Math.floor(options.limit)
|
|
549
541
|
: 8;
|
|
550
|
-
const
|
|
551
|
-
const activeEntries = entries.filter((entry) => !isSupersededLearning(entry, staleTriggers));
|
|
552
|
-
const ranked = activeEntries.map((entry, index) => {
|
|
542
|
+
const ranked = entries.map((entry, index) => {
|
|
553
543
|
let score = 0;
|
|
554
544
|
let stageScore = 0;
|
|
555
545
|
if (stage) {
|
|
@@ -567,13 +557,9 @@ export async function selectRelevantLearnings(projectRoot, options = {}) {
|
|
|
567
557
|
score += 1;
|
|
568
558
|
if (entry.frequency >= 3)
|
|
569
559
|
score += 1;
|
|
570
|
-
if (entry.maturity === "lifted-to-enforcement")
|
|
571
|
-
score -= 1;
|
|
572
560
|
const searchable = [
|
|
573
|
-
...tokenizeText(entry.domain),
|
|
574
561
|
...tokenizeText(entry.trigger),
|
|
575
562
|
...tokenizeText(entry.action),
|
|
576
|
-
...tokenizeText(entry.origin_run),
|
|
577
563
|
...tokenizeText(entry.project)
|
|
578
564
|
];
|
|
579
565
|
const searchSet = new Set(searchable);
|
package/dist/policy.js
CHANGED
|
@@ -58,7 +58,7 @@ export async function policyChecks(projectRoot, options = {}) {
|
|
|
58
58
|
const utilitySkillChecks = [
|
|
59
59
|
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "strict JSONL schema", name: "utility_skill:learnings:jsonl_schema" },
|
|
60
60
|
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "knowledge.jsonl", name: "utility_skill:learnings:jsonl_store" },
|
|
61
|
-
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "type, trigger, action, confidence,
|
|
61
|
+
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "type, trigger, action, confidence, stage, origin_stage, frequency, created, first_seen_ts, last_seen_ts, project", name: "utility_skill:learnings:field_order" },
|
|
62
62
|
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Manual Actions", name: "utility_skill:learnings:manual_actions" },
|
|
63
63
|
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:learnings:hard_gate" },
|
|
64
64
|
{ file: runtimeFile("commands/start.md"), needle: "## Algorithm", name: "utility_command:start:algorithm" },
|
|
@@ -71,7 +71,7 @@ export async function policyChecks(projectRoot, options = {}) {
|
|
|
71
71
|
{ file: runtimeFile("skills/flow-view/SKILL.md"), needle: "## Diff Subcommand", name: "utility_skill:view:diff_section" },
|
|
72
72
|
{ file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:sdd:hard_gate" },
|
|
73
73
|
{ file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "## Status Contract", name: "utility_skill:sdd:status_contract" },
|
|
74
|
-
{ file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "
|
|
74
|
+
{ file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "slice-implementer", name: "utility_skill:sdd:implementer_template" },
|
|
75
75
|
{ file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "## Model & Harness Routing Notes", name: "utility_skill:sdd:routing_notes" },
|
|
76
76
|
{ file: runtimeFile("skills/parallel-dispatch/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:parallel:hard_gate" },
|
|
77
77
|
{ file: runtimeFile("skills/parallel-dispatch/SKILL.md"), needle: "Review Army", name: "utility_skill:parallel:review_army" },
|
|
@@ -89,7 +89,7 @@ export async function policyChecks(projectRoot, options = {}) {
|
|
|
89
89
|
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Task classification", name: "meta_skill:task_classification" },
|
|
90
90
|
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Stage quick map", name: "meta_skill:stage_quick_map" },
|
|
91
91
|
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Whole flow map", name: "meta_skill:whole_flow_map" },
|
|
92
|
-
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "
|
|
92
|
+
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "post_ship_review -> archive", name: "meta_skill:closeout_chain" },
|
|
93
93
|
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Contextual Skill Activation", name: "meta_skill:contextual_skills" },
|
|
94
94
|
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Protocol Behavior", name: "meta_skill:protocol_behavior" },
|
|
95
95
|
{ file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Failure guardrails", name: "meta_skill:failure_guardrails" },
|
package/dist/retro-gate.js
CHANGED
|
@@ -1,34 +1,19 @@
|
|
|
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
|
|
5
|
-
import { readKnowledgeSafely } from "./knowledge-store.js";
|
|
4
|
+
import { exists } from "./fs-utils.js";
|
|
6
5
|
function activeArtifactsPath(projectRoot) {
|
|
7
6
|
return path.join(projectRoot, RUNTIME_ROOT, "artifacts");
|
|
8
7
|
}
|
|
9
8
|
function retroArtifactPath(projectRoot) {
|
|
10
9
|
return path.join(activeArtifactsPath(projectRoot), "09-retro.md");
|
|
11
10
|
}
|
|
12
|
-
// Fallback window for compound-entry scanning when `retroDraftedAt` /
|
|
13
|
-
// `retroAcceptedAt` are not set (legacy runs or imports): use the retro
|
|
14
|
-
// artifact's mtime ± 7 days. 24h was too narrow for long-running retros
|
|
15
|
-
// that are edited over several days or runs imported from another
|
|
16
|
-
// machine with slightly different clocks; 7 days is still tight enough
|
|
17
|
-
// that entries from an unrelated future run are excluded.
|
|
18
|
-
const RETRO_ARTIFACT_MTIME_FALLBACK_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
19
11
|
function parseIsoTimestamp(value) {
|
|
20
12
|
if (!value || value.trim().length === 0)
|
|
21
13
|
return null;
|
|
22
14
|
const parsed = Date.parse(value);
|
|
23
15
|
return Number.isFinite(parsed) ? parsed : null;
|
|
24
16
|
}
|
|
25
|
-
function inInclusiveWindow(timestamp, windowStartMs, windowEndMs) {
|
|
26
|
-
if (windowStartMs !== null && timestamp < windowStartMs)
|
|
27
|
-
return false;
|
|
28
|
-
if (windowEndMs !== null && timestamp > windowEndMs)
|
|
29
|
-
return false;
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
17
|
export async function evaluateRetroGate(projectRoot, state) {
|
|
33
18
|
const required = state.completedStages.includes("ship");
|
|
34
19
|
const artifactFile = retroArtifactPath(projectRoot);
|
|
@@ -42,83 +27,16 @@ export async function evaluateRetroGate(projectRoot, state) {
|
|
|
42
27
|
hasRetroArtifact = false;
|
|
43
28
|
}
|
|
44
29
|
}
|
|
45
|
-
let compoundEntries = 0;
|
|
46
|
-
let windowStartMs = parseIsoTimestamp(state.closeout.retroDraftedAt);
|
|
47
|
-
let windowEndMs = parseIsoTimestamp(state.closeout.retroAcceptedAt) ?? parseIsoTimestamp(state.retro.completedAt);
|
|
48
|
-
if (hasRetroArtifact &&
|
|
49
|
-
windowStartMs === null &&
|
|
50
|
-
windowEndMs === null) {
|
|
51
|
-
try {
|
|
52
|
-
const stats = await fs.stat(artifactFile);
|
|
53
|
-
const anchor = stats.mtimeMs;
|
|
54
|
-
if (Number.isFinite(anchor) && anchor > 0) {
|
|
55
|
-
windowStartMs = anchor - RETRO_ARTIFACT_MTIME_FALLBACK_WINDOW_MS;
|
|
56
|
-
windowEndMs = anchor + RETRO_ARTIFACT_MTIME_FALLBACK_WINDOW_MS;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// fallback scan remains disabled when mtime cannot be read
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const shouldScanCompoundEvidence = windowStartMs !== null || windowEndMs !== null;
|
|
64
|
-
if (shouldScanCompoundEvidence) {
|
|
65
|
-
const countIfEligible = (parsed) => {
|
|
66
|
-
if (parsed.type !== "compound") {
|
|
67
|
-
return 0;
|
|
68
|
-
}
|
|
69
|
-
const created = typeof parsed.created === "string" ? parseIsoTimestamp(parsed.created) : null;
|
|
70
|
-
if (created === null || !inInclusiveWindow(created, windowStartMs, windowEndMs)) {
|
|
71
|
-
return 0;
|
|
72
|
-
}
|
|
73
|
-
const source = typeof parsed.source === "string"
|
|
74
|
-
? parsed.source.trim().toLowerCase()
|
|
75
|
-
: null;
|
|
76
|
-
const legacyRetroStage = parsed.stage === "retro";
|
|
77
|
-
return source === "retro" || legacyRetroStage ? 1 : 0;
|
|
78
|
-
};
|
|
79
|
-
try {
|
|
80
|
-
const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|
|
81
|
-
const { entries } = await readKnowledgeSafely(projectRoot);
|
|
82
|
-
for (const parsed of entries) {
|
|
83
|
-
compoundEntries += countIfEligible(parsed);
|
|
84
|
-
}
|
|
85
|
-
// Backward compatibility for historical/hand-edited rows that don't pass
|
|
86
|
-
// strict knowledge schema validation but still carry retro evidence.
|
|
87
|
-
if (compoundEntries === 0 && (await exists(knowledgeFile))) {
|
|
88
|
-
const raw = stripBom(await fs.readFile(knowledgeFile, "utf8"));
|
|
89
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
90
|
-
const trimmed = line.trim();
|
|
91
|
-
if (!trimmed)
|
|
92
|
-
continue;
|
|
93
|
-
try {
|
|
94
|
-
const parsed = JSON.parse(trimmed);
|
|
95
|
-
compoundEntries += countIfEligible(parsed);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
// ignore malformed lines for retro gate calculation
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
compoundEntries = 0;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// A retro is considered complete when any of:
|
|
108
|
-
// - the retro artifact exists AND (at least one compound learning was
|
|
109
|
-
// promoted during the retro window OR compound was explicitly skipped
|
|
110
|
-
// after reviewing the draft), or
|
|
111
|
-
// - the operator explicitly skipped the retro step itself
|
|
112
|
-
// (`retroSkipped === true` with a non-empty reason). `retroSkipped` is an
|
|
113
|
-
// operator-level override of the artifact requirement, so it must
|
|
114
|
-
// bypass `hasRetroArtifact` — otherwise a run that legitimately had
|
|
115
|
-
// nothing worth retro-ing dead-locks at closeout waiting for a
|
|
116
|
-
// file that will never exist.
|
|
117
30
|
const retroSkipReason = state.closeout.retroSkipReason?.trim() ?? "";
|
|
118
31
|
const retroSkipped = state.closeout.retroSkipped === true && retroSkipReason.length > 0;
|
|
32
|
+
const retroAccepted = hasRetroArtifact && parseIsoTimestamp(state.closeout.retroAcceptedAt) !== null;
|
|
119
33
|
const compoundSkipped = state.closeout.compoundSkipped === true;
|
|
120
|
-
const
|
|
121
|
-
|
|
34
|
+
const compoundReviewed = parseIsoTimestamp(state.closeout.compoundCompletedAt) !== null ||
|
|
35
|
+
state.closeout.compoundPromoted > 0;
|
|
36
|
+
const compoundEntries = compoundReviewed ? Math.max(0, Math.floor(state.closeout.compoundPromoted)) : 0;
|
|
37
|
+
// Keep retro-gate deterministic from closeout state only:
|
|
38
|
+
// retroComplete = (retroAccepted || retroSkipped) && (compoundReviewed || compoundSkipped)
|
|
39
|
+
const completed = required ? (retroAccepted || retroSkipped) && (compoundReviewed || compoundSkipped) : true;
|
|
122
40
|
return {
|
|
123
41
|
required,
|
|
124
42
|
completed,
|
package/dist/run-archive.js
CHANGED
|
@@ -17,12 +17,10 @@ const STATE_SNAPSHOT_EXCLUDE = new Set([
|
|
|
17
17
|
]);
|
|
18
18
|
const DELEGATION_LOG_FILE = "delegation-log.json";
|
|
19
19
|
const TDD_CYCLE_LOG_FILE = "tdd-cycle-log.jsonl";
|
|
20
|
-
const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
|
|
21
20
|
const CRITICAL_STATE_SNAPSHOT_FILES = new Set([
|
|
22
21
|
"flow-state.json",
|
|
23
22
|
DELEGATION_LOG_FILE,
|
|
24
|
-
TDD_CYCLE_LOG_FILE
|
|
25
|
-
RECONCILIATION_NOTICES_FILE
|
|
23
|
+
TDD_CYCLE_LOG_FILE
|
|
26
24
|
]);
|
|
27
25
|
function archiveRoot(projectRoot) {
|
|
28
26
|
return path.join(projectRoot, ARCHIVE_DIR_REL_PATH);
|
|
@@ -89,7 +87,6 @@ async function resetCarryoverStateFiles(projectRoot, activeRunId) {
|
|
|
89
87
|
await ensureDir(stateDir);
|
|
90
88
|
await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n`, { mode: 0o600 });
|
|
91
89
|
await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "", { mode: 0o600 });
|
|
92
|
-
await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`, { mode: 0o600 });
|
|
93
90
|
}
|
|
94
91
|
async function restoreStateSnapshot(projectRoot, archiveStatePath) {
|
|
95
92
|
if (!(await exists(archiveStatePath)))
|
package/dist/run-persistence.js
CHANGED
|
@@ -17,11 +17,6 @@ export class InvalidStageTransitionError extends Error {
|
|
|
17
17
|
const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
|
|
18
18
|
const ARCHIVE_DIR_REL_PATH = `${RUNTIME_ROOT}/archive`;
|
|
19
19
|
const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
|
|
20
|
-
const RECONCILIATION_NOTICES_REL_PATH = `${RUNTIME_ROOT}/state/reconciliation-notices.json`;
|
|
21
|
-
const RECONCILIATION_NOTICES_LOCK_REL_PATH = `${RUNTIME_ROOT}/state/.reconciliation-notices.lock`;
|
|
22
|
-
const RECONCILIATION_NOTICES_SCHEMA_VERSION = 1;
|
|
23
|
-
const CLOSEOUT_SUBSTATE_DEMOTION_KIND = "closeout_substate_demotion";
|
|
24
|
-
const CLOSEOUT_SUBSTATE_GATE_ID = "closeout.shipSubstate";
|
|
25
20
|
const FLOW_STAGE_SET = new Set(FLOW_STAGES);
|
|
26
21
|
function validateFlowTransition(prev, next) {
|
|
27
22
|
if (prev.activeRunId !== next.activeRunId) {
|
|
@@ -74,88 +69,12 @@ function flowStatePath(projectRoot) {
|
|
|
74
69
|
function flowStateLockPath(projectRoot) {
|
|
75
70
|
return path.join(projectRoot, RUNTIME_ROOT, "state", ".flow-state.lock");
|
|
76
71
|
}
|
|
77
|
-
function reconciliationNoticesPath(projectRoot) {
|
|
78
|
-
return path.join(projectRoot, RECONCILIATION_NOTICES_REL_PATH);
|
|
79
|
-
}
|
|
80
|
-
function reconciliationNoticesLockPath(projectRoot) {
|
|
81
|
-
return path.join(projectRoot, RECONCILIATION_NOTICES_LOCK_REL_PATH);
|
|
82
|
-
}
|
|
83
72
|
function archiveRoot(projectRoot) {
|
|
84
73
|
return path.join(projectRoot, ARCHIVE_DIR_REL_PATH);
|
|
85
74
|
}
|
|
86
75
|
function activeArtifactsPath(projectRoot) {
|
|
87
76
|
return path.join(projectRoot, ACTIVE_ARTIFACTS_REL_PATH);
|
|
88
77
|
}
|
|
89
|
-
function asObjectRecord(value) {
|
|
90
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
91
|
-
return null;
|
|
92
|
-
return value;
|
|
93
|
-
}
|
|
94
|
-
async function appendCloseoutSubstateDemotionNotice(projectRoot, state, demotion) {
|
|
95
|
-
await withDirectoryLock(reconciliationNoticesLockPath(projectRoot), async () => {
|
|
96
|
-
const filePath = reconciliationNoticesPath(projectRoot);
|
|
97
|
-
const existingNotices = [];
|
|
98
|
-
if (await exists(filePath)) {
|
|
99
|
-
try {
|
|
100
|
-
const raw = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
101
|
-
const parsed = asObjectRecord(raw);
|
|
102
|
-
const noticesRaw = parsed?.notices;
|
|
103
|
-
if (Array.isArray(noticesRaw)) {
|
|
104
|
-
for (const notice of noticesRaw) {
|
|
105
|
-
const typed = asObjectRecord(notice);
|
|
106
|
-
if (typed)
|
|
107
|
-
existingNotices.push(typed);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Keep going with an empty payload; sync can still report parse errors.
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
const alreadyRecorded = existingNotices.some((notice) => {
|
|
116
|
-
if (notice.runId !== state.activeRunId)
|
|
117
|
-
return false;
|
|
118
|
-
if (notice.kind !== CLOSEOUT_SUBSTATE_DEMOTION_KIND)
|
|
119
|
-
return false;
|
|
120
|
-
const payload = asObjectRecord(notice.payload);
|
|
121
|
-
return payload?.previous === demotion.previous &&
|
|
122
|
-
payload?.next === demotion.next &&
|
|
123
|
-
payload?.reason === demotion.reason;
|
|
124
|
-
});
|
|
125
|
-
if (alreadyRecorded)
|
|
126
|
-
return;
|
|
127
|
-
const ts = new Date().toISOString();
|
|
128
|
-
existingNotices.push({
|
|
129
|
-
id: `${state.activeRunId}:${state.currentStage}:${CLOSEOUT_SUBSTATE_GATE_ID}:${CLOSEOUT_SUBSTATE_DEMOTION_KIND}:${ts}`,
|
|
130
|
-
runId: state.activeRunId,
|
|
131
|
-
stage: state.currentStage,
|
|
132
|
-
gateId: CLOSEOUT_SUBSTATE_GATE_ID,
|
|
133
|
-
reason: demotion.reason,
|
|
134
|
-
demotedAt: ts,
|
|
135
|
-
kind: CLOSEOUT_SUBSTATE_DEMOTION_KIND,
|
|
136
|
-
payload: {
|
|
137
|
-
previous: demotion.previous,
|
|
138
|
-
next: demotion.next,
|
|
139
|
-
reason: demotion.reason
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
existingNotices.sort((left, right) => {
|
|
143
|
-
const leftTs = typeof left.demotedAt === "string" ? left.demotedAt : "";
|
|
144
|
-
const rightTs = typeof right.demotedAt === "string" ? right.demotedAt : "";
|
|
145
|
-
if (leftTs === rightTs) {
|
|
146
|
-
const leftId = typeof left.id === "string" ? left.id : "";
|
|
147
|
-
const rightId = typeof right.id === "string" ? right.id : "";
|
|
148
|
-
return leftId.localeCompare(rightId);
|
|
149
|
-
}
|
|
150
|
-
return leftTs.localeCompare(rightTs);
|
|
151
|
-
});
|
|
152
|
-
await ensureDir(path.dirname(filePath));
|
|
153
|
-
await writeFileSafe(filePath, `${JSON.stringify({
|
|
154
|
-
schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
|
|
155
|
-
notices: existingNotices
|
|
156
|
-
}, null, 2)}\n`, { mode: 0o600 });
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
78
|
function isFlowStage(value) {
|
|
160
79
|
return typeof value === "string" && FLOW_STAGE_SET.has(value);
|
|
161
80
|
}
|
|
@@ -338,14 +257,20 @@ function sanitizeRetroState(value) {
|
|
|
338
257
|
function isShipSubstate(value) {
|
|
339
258
|
return typeof value === "string" && SHIP_SUBSTATES.includes(value);
|
|
340
259
|
}
|
|
341
|
-
function sanitizeCloseoutState(value
|
|
260
|
+
function sanitizeCloseoutState(value) {
|
|
342
261
|
const fallback = createInitialCloseoutState();
|
|
343
262
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
344
263
|
return fallback;
|
|
345
264
|
}
|
|
346
265
|
const typed = value;
|
|
347
|
-
|
|
348
|
-
|
|
266
|
+
const rawShipSubstate = typeof typed.shipSubstate === "string" ? typed.shipSubstate : undefined;
|
|
267
|
+
let shipSubstate;
|
|
268
|
+
if (rawShipSubstate === "retro_review" || rawShipSubstate === "compound_review") {
|
|
269
|
+
shipSubstate = "post_ship_review";
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
shipSubstate = isShipSubstate(rawShipSubstate) ? rawShipSubstate : fallback.shipSubstate;
|
|
273
|
+
}
|
|
349
274
|
const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
|
|
350
275
|
const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
|
|
351
276
|
const retroSkipReason = typeof typed.retroSkipReason === "string"
|
|
@@ -370,21 +295,8 @@ function sanitizeCloseoutState(value, onDemotion) {
|
|
|
370
295
|
// the compound leg, which would let `archive` skip durable closeout proof.
|
|
371
296
|
const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
|
|
372
297
|
const compoundDone = compoundCompletedAt !== undefined || compoundPromoted > 0 || compoundSkipped === true;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
shipSubstate = "retro_review";
|
|
376
|
-
demotionReason = "retro closeout leg is incomplete (missing retroAcceptedAt or explicit retro skip).";
|
|
377
|
-
}
|
|
378
|
-
else if (shipSubstate === "ready_to_archive" && !compoundDone) {
|
|
379
|
-
shipSubstate = "compound_review";
|
|
380
|
-
demotionReason = "compound closeout leg is incomplete (missing compound proof).";
|
|
381
|
-
}
|
|
382
|
-
if (demotionReason && previousShipSubstate !== shipSubstate) {
|
|
383
|
-
onDemotion?.({
|
|
384
|
-
previous: previousShipSubstate,
|
|
385
|
-
next: shipSubstate,
|
|
386
|
-
reason: demotionReason
|
|
387
|
-
});
|
|
298
|
+
if (shipSubstate === "ready_to_archive" && (!retroDone || !compoundDone)) {
|
|
299
|
+
shipSubstate = "post_ship_review";
|
|
388
300
|
}
|
|
389
301
|
return {
|
|
390
302
|
shipSubstate,
|
|
@@ -405,7 +317,6 @@ function coerceFlowState(parsed) {
|
|
|
405
317
|
const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
|
|
406
318
|
? activeRunIdRaw.trim()
|
|
407
319
|
: next.activeRunId;
|
|
408
|
-
let closeoutDemotion;
|
|
409
320
|
const state = {
|
|
410
321
|
schemaVersion: FLOW_STATE_SCHEMA_VERSION,
|
|
411
322
|
activeRunId,
|
|
@@ -418,11 +329,9 @@ function coerceFlowState(parsed) {
|
|
|
418
329
|
staleStages: sanitizeStaleStages(parsed.staleStages),
|
|
419
330
|
rewinds: sanitizeRewinds(parsed.rewinds),
|
|
420
331
|
retro: sanitizeRetroState(parsed.retro),
|
|
421
|
-
closeout: sanitizeCloseoutState(parsed.closeout
|
|
422
|
-
closeoutDemotion = demotion;
|
|
423
|
-
})
|
|
332
|
+
closeout: sanitizeCloseoutState(parsed.closeout)
|
|
424
333
|
};
|
|
425
|
-
return { state
|
|
334
|
+
return { state };
|
|
426
335
|
}
|
|
427
336
|
export class CorruptFlowStateError extends Error {
|
|
428
337
|
statePath;
|
|
@@ -484,11 +393,7 @@ export async function readFlowState(projectRoot, options = {}) {
|
|
|
484
393
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
485
394
|
await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
|
|
486
395
|
}
|
|
487
|
-
|
|
488
|
-
if (coerced.closeoutDemotion) {
|
|
489
|
-
await appendCloseoutSubstateDemotionNotice(projectRoot, coerced.state, coerced.closeoutDemotion);
|
|
490
|
-
}
|
|
491
|
-
return coerced.state;
|
|
396
|
+
return coerceFlowState(parsed).state;
|
|
492
397
|
}
|
|
493
398
|
export async function writeFlowState(projectRoot, state, options = {}) {
|
|
494
399
|
const doWrite = async () => {
|