cclaw-cli 0.46.14 → 0.47.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 +16 -11
- package/dist/artifact-linter.d.ts +2 -0
- package/dist/artifact-linter.js +137 -9
- package/dist/config.d.ts +12 -7
- package/dist/config.js +79 -10
- package/dist/content/compound-command.d.ts +5 -2
- package/dist/content/compound-command.js +47 -16
- package/dist/content/contracts.js +1 -1
- package/dist/content/examples.d.ts +1 -0
- package/dist/content/examples.js +13 -0
- package/dist/content/harnesses-doc.js +11 -0
- package/dist/content/learnings.d.ts +2 -1
- package/dist/content/learnings.js +5 -3
- package/dist/content/observe.d.ts +2 -1
- package/dist/content/observe.js +174 -14
- package/dist/content/stage-schema.js +21 -8
- package/dist/content/stages/design.js +0 -2
- package/dist/content/utility-skills.js +1 -1
- package/dist/delegation.js +19 -4
- package/dist/gate-evidence.js +6 -1
- package/dist/install.d.ts +3 -3
- package/dist/install.js +13 -8
- package/dist/internal/advance-stage.js +53 -16
- package/dist/knowledge-store.d.ts +3 -0
- package/dist/knowledge-store.js +11 -1
- package/dist/retro-gate.js +11 -1
- package/dist/run-persistence.js +11 -1
- package/dist/tdd-cycle.js +19 -1
- package/dist/types.d.ts +30 -2
- package/package.json +1 -1
|
@@ -16,21 +16,36 @@ function unique(values) {
|
|
|
16
16
|
const TEST_COMMAND_HINT_PATTERN = /\b(?:npm test|pnpm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)\b/iu;
|
|
17
17
|
const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
|
|
18
18
|
const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const SHIP_FINALIZATION_MODE_PATTERN = /\bFINALIZE_(?:MERGE_LOCAL|OPEN_PR|QUEUE|HANDOFF|SKIP)\b/u;
|
|
20
|
+
// Per-gate validators keyed by `${stage}:${gateId}`. Returning a non-null
|
|
21
|
+
// string surfaces the reason as an `advance-stage` failure so evidence is
|
|
22
|
+
// guaranteed to carry the structural breadcrumbs downstream tooling
|
|
23
|
+
// expects. Previously only `tdd:tdd_verified_before_complete` was checked.
|
|
24
|
+
const GATE_EVIDENCE_VALIDATORS = {
|
|
25
|
+
"tdd:tdd_verified_before_complete": (evidence) => {
|
|
26
|
+
if (!TEST_COMMAND_HINT_PATTERN.test(evidence)) {
|
|
27
|
+
return "must include the fresh verification command that was run (for example `npm test`, `pytest`, `go test`, or equivalent).";
|
|
28
|
+
}
|
|
29
|
+
if (!SHA_WITH_LABEL_PATTERN.test(evidence)) {
|
|
30
|
+
return "must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).";
|
|
31
|
+
}
|
|
32
|
+
if (!PASS_STATUS_PATTERN.test(evidence)) {
|
|
33
|
+
return "must include explicit success status (for example `PASS` or `GREEN`).";
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
"ship:ship_finalization_executed": (evidence) => {
|
|
38
|
+
if (!SHIP_FINALIZATION_MODE_PATTERN.test(evidence)) {
|
|
39
|
+
return "must name the finalization mode that ran (for example `FINALIZE_MERGE_LOCAL`, `FINALIZE_OPEN_PR`, `FINALIZE_HANDOFF`, `FINALIZE_QUEUE`, or `FINALIZE_SKIP`).";
|
|
40
|
+
}
|
|
21
41
|
return null;
|
|
22
42
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
if (!PASS_STATUS_PATTERN.test(trimmed)) {
|
|
31
|
-
return "must include explicit success status (for example `PASS` or `GREEN`).";
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
43
|
+
};
|
|
44
|
+
function validateGateEvidenceShape(stage, gateId, evidence) {
|
|
45
|
+
const validator = GATE_EVIDENCE_VALIDATORS[`${stage}:${gateId}`];
|
|
46
|
+
if (!validator)
|
|
47
|
+
return null;
|
|
48
|
+
return validator(evidence.trim());
|
|
34
49
|
}
|
|
35
50
|
function parseStringList(raw) {
|
|
36
51
|
if (!Array.isArray(raw))
|
|
@@ -58,10 +73,23 @@ function parseGuardEvidence(value) {
|
|
|
58
73
|
}
|
|
59
74
|
return next;
|
|
60
75
|
}
|
|
76
|
+
function emptyGateState() {
|
|
77
|
+
return {
|
|
78
|
+
required: [],
|
|
79
|
+
recommended: [],
|
|
80
|
+
conditional: [],
|
|
81
|
+
triggered: [],
|
|
82
|
+
passed: [],
|
|
83
|
+
blocked: []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
61
86
|
function parseCandidateGateCatalog(value, fallback) {
|
|
62
87
|
const next = {};
|
|
63
88
|
for (const stage of FLOW_STAGES) {
|
|
64
|
-
|
|
89
|
+
// Guard against stale on-disk flow-state files that persisted a partial
|
|
90
|
+
// stageGateCatalog (missing a stage key). Previously `fallback[stage]`
|
|
91
|
+
// could be undefined and the spread below would throw at runtime.
|
|
92
|
+
const base = fallback[stage] ?? emptyGateState();
|
|
65
93
|
next[stage] = {
|
|
66
94
|
required: [...base.required],
|
|
67
95
|
recommended: [...base.recommended],
|
|
@@ -81,7 +109,7 @@ function parseCandidateGateCatalog(value, fallback) {
|
|
|
81
109
|
continue;
|
|
82
110
|
}
|
|
83
111
|
const typed = rawStage;
|
|
84
|
-
const base = fallback[stage];
|
|
112
|
+
const base = fallback[stage] ?? emptyGateState();
|
|
85
113
|
const allowed = new Set([...base.required, ...base.recommended, ...base.conditional]);
|
|
86
114
|
const conditional = new Set(base.conditional);
|
|
87
115
|
const passed = unique(parseStringList(typed.passed)).filter((gateId) => allowed.has(gateId));
|
|
@@ -114,13 +142,22 @@ function coerceCandidateFlowState(raw, fallback) {
|
|
|
114
142
|
const completedStages = unique(parseStringList(typed.completedStages).filter((stage) => isFlowStageValue(stage)));
|
|
115
143
|
const skippedStagesRaw = parseStringList(typed.skippedStages).filter((stage) => isFlowStageValue(stage));
|
|
116
144
|
const skippedStages = skippedStagesRaw.length > 0 ? skippedStagesRaw : fallback.skippedStages;
|
|
145
|
+
// When the candidate payload omits `guardEvidence` entirely we must keep
|
|
146
|
+
// the on-disk fallback — otherwise a partial update (e.g. a tooling call
|
|
147
|
+
// that only passes stage + passedGateIds) would silently wipe every
|
|
148
|
+
// previously recorded evidence string and fail the next
|
|
149
|
+
// `verifyCurrentStageGateEvidence` check.
|
|
150
|
+
const candidateEvidence = parseGuardEvidence(typed.guardEvidence);
|
|
151
|
+
const guardEvidence = typed.guardEvidence === undefined
|
|
152
|
+
? { ...fallback.guardEvidence }
|
|
153
|
+
: candidateEvidence;
|
|
117
154
|
return {
|
|
118
155
|
...fallback,
|
|
119
156
|
currentStage,
|
|
120
157
|
completedStages,
|
|
121
158
|
track,
|
|
122
159
|
skippedStages,
|
|
123
|
-
guardEvidence
|
|
160
|
+
guardEvidence,
|
|
124
161
|
stageGateCatalog: parseCandidateGateCatalog(typed.stageGateCatalog, fallback.stageGateCatalog)
|
|
125
162
|
};
|
|
126
163
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FlowStage } from "./types.js";
|
|
2
2
|
export type KnowledgeEntryType = "rule" | "pattern" | "lesson" | "compound";
|
|
3
3
|
export type KnowledgeEntryConfidence = "high" | "medium" | "low";
|
|
4
|
+
export type KnowledgeEntrySeverity = "critical" | "important" | "suggestion";
|
|
4
5
|
export type KnowledgeEntryUniversality = "project" | "personal" | "universal";
|
|
5
6
|
export type KnowledgeEntryMaturity = "raw" | "lifted-to-rule" | "lifted-to-enforcement";
|
|
6
7
|
export type KnowledgeEntrySource = "stage" | "retro" | "compound" | "ideate" | "manual";
|
|
@@ -9,6 +10,7 @@ export interface KnowledgeEntry {
|
|
|
9
10
|
trigger: string;
|
|
10
11
|
action: string;
|
|
11
12
|
confidence: KnowledgeEntryConfidence;
|
|
13
|
+
severity?: KnowledgeEntrySeverity;
|
|
12
14
|
domain: string | null;
|
|
13
15
|
stage: FlowStage | null;
|
|
14
16
|
origin_stage: FlowStage | null;
|
|
@@ -27,6 +29,7 @@ export interface KnowledgeSeedEntry {
|
|
|
27
29
|
trigger: string;
|
|
28
30
|
action: string;
|
|
29
31
|
confidence: KnowledgeEntryConfidence;
|
|
32
|
+
severity?: KnowledgeEntrySeverity;
|
|
30
33
|
domain?: string | null;
|
|
31
34
|
stage?: FlowStage | null;
|
|
32
35
|
origin_stage?: FlowStage | null;
|
package/dist/knowledge-store.js
CHANGED
|
@@ -5,6 +5,7 @@ import { 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"]);
|
|
8
|
+
const KNOWLEDGE_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
|
|
8
9
|
const KNOWLEDGE_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
|
|
9
10
|
const KNOWLEDGE_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
|
|
10
11
|
const KNOWLEDGE_SOURCE_SET = new Set([
|
|
@@ -34,6 +35,7 @@ const KNOWLEDGE_REQUIRED_KEYS = [
|
|
|
34
35
|
];
|
|
35
36
|
const KNOWLEDGE_ALLOWED_KEYS = new Set(KNOWLEDGE_REQUIRED_KEYS);
|
|
36
37
|
KNOWLEDGE_ALLOWED_KEYS.add("source");
|
|
38
|
+
KNOWLEDGE_ALLOWED_KEYS.add("severity");
|
|
37
39
|
function knowledgePath(projectRoot) {
|
|
38
40
|
return path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|
|
39
41
|
}
|
|
@@ -60,7 +62,8 @@ function dedupeKey(entry) {
|
|
|
60
62
|
entry.origin_feature === null ? "null" : normalizeText(entry.origin_feature),
|
|
61
63
|
entry.universality,
|
|
62
64
|
entry.project === null ? "null" : normalizeText(entry.project),
|
|
63
|
-
entry.source === undefined || entry.source === null ? "null" : entry.source
|
|
65
|
+
entry.source === undefined || entry.source === null ? "null" : entry.source,
|
|
66
|
+
entry.severity === undefined ? "none" : entry.severity
|
|
64
67
|
].join("|");
|
|
65
68
|
}
|
|
66
69
|
function isIsoUtcTimestamp(value) {
|
|
@@ -100,6 +103,10 @@ export function validateKnowledgeEntry(entry) {
|
|
|
100
103
|
if (!KNOWLEDGE_CONFIDENCE_SET.has(obj.confidence)) {
|
|
101
104
|
errors.push("confidence must be one of: high, medium, low.");
|
|
102
105
|
}
|
|
106
|
+
if (obj.severity !== undefined &&
|
|
107
|
+
(typeof obj.severity !== "string" || !KNOWLEDGE_SEVERITY_SET.has(obj.severity))) {
|
|
108
|
+
errors.push("severity must be one of: critical, important, suggestion.");
|
|
109
|
+
}
|
|
103
110
|
if (!isNullableString(obj.domain)) {
|
|
104
111
|
errors.push("domain must be string or null.");
|
|
105
112
|
}
|
|
@@ -161,6 +168,9 @@ export function materializeKnowledgeEntry(seed, defaults = {}) {
|
|
|
161
168
|
last_seen_ts: normalizeUtcIso(seed.last_seen_ts ?? now),
|
|
162
169
|
project: seed.project ?? defaults.project ?? null
|
|
163
170
|
};
|
|
171
|
+
if (seed.severity !== undefined) {
|
|
172
|
+
entry.severity = seed.severity;
|
|
173
|
+
}
|
|
164
174
|
if (source !== null) {
|
|
165
175
|
entry.source = source;
|
|
166
176
|
}
|
package/dist/retro-gate.js
CHANGED
|
@@ -73,7 +73,17 @@ export async function evaluateRetroGate(projectRoot, state) {
|
|
|
73
73
|
compoundEntries = 0;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
// A retro is considered complete when either:
|
|
77
|
+
// - at least one compound learning was promoted during the retro window, or
|
|
78
|
+
// - the operator explicitly skipped retro or compound (`retroSkipped` /
|
|
79
|
+
// `compoundSkipped` recorded in the closeout substate) after reviewing
|
|
80
|
+
// the draft. Previously the gate required `compoundEntries > 0`
|
|
81
|
+
// unconditionally, which dead-locked ship closeout whenever the retro
|
|
82
|
+
// yielded no new patterns worth promoting.
|
|
83
|
+
const explicitSkip = Boolean(state.closeout.retroSkipped || state.closeout.compoundSkipped);
|
|
84
|
+
const completed = required
|
|
85
|
+
? hasRetroArtifact && (compoundEntries > 0 || explicitSkip)
|
|
86
|
+
: true;
|
|
77
87
|
return {
|
|
78
88
|
required,
|
|
79
89
|
completed,
|
package/dist/run-persistence.js
CHANGED
|
@@ -235,7 +235,7 @@ function sanitizeCloseoutState(value) {
|
|
|
235
235
|
return fallback;
|
|
236
236
|
}
|
|
237
237
|
const typed = value;
|
|
238
|
-
|
|
238
|
+
let shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
|
|
239
239
|
const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
|
|
240
240
|
const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
|
|
241
241
|
const retroSkipped = typeof typed.retroSkipped === "boolean" ? typed.retroSkipped : undefined;
|
|
@@ -246,6 +246,16 @@ function sanitizeCloseoutState(value) {
|
|
|
246
246
|
const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
|
|
247
247
|
? Math.floor(promotedRaw)
|
|
248
248
|
: 0;
|
|
249
|
+
// Demote shipSubstate when its retro invariant is violated on disk. A
|
|
250
|
+
// hand-edited flow-state could claim `ready_to_archive` or `compound_review`
|
|
251
|
+
// without ever going through the retro step, which would let `archive`
|
|
252
|
+
// proceed and skip the gate. Compound completion is not independently
|
|
253
|
+
// tracked in all flows (some runs rely on knowledge.jsonl + the retro
|
|
254
|
+
// window), so we only demote when the retro leg is missing outright.
|
|
255
|
+
const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
|
|
256
|
+
if (!retroDone && (shipSubstate === "ready_to_archive" || shipSubstate === "compound_review")) {
|
|
257
|
+
shipSubstate = "retro_review";
|
|
258
|
+
}
|
|
249
259
|
return {
|
|
250
260
|
shipSubstate,
|
|
251
261
|
retroDraftedAt,
|
package/dist/tdd-cycle.js
CHANGED
|
@@ -31,6 +31,7 @@ export function parseTddCycleLog(text) {
|
|
|
31
31
|
}
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
|
+
const SLICE_ID_PATTERN = /^S-\d+$/u;
|
|
34
35
|
export function validateTddCycleOrder(entries, options = {}) {
|
|
35
36
|
const targetRun = options.runId;
|
|
36
37
|
const filtered = targetRun
|
|
@@ -44,6 +45,15 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
44
45
|
}
|
|
45
46
|
const issues = [];
|
|
46
47
|
const openRedSlices = [];
|
|
48
|
+
// Reject slices whose ID does not match the stable `S-<number>` contract.
|
|
49
|
+
// Entries that drop the slice field entirely were previously coerced to
|
|
50
|
+
// `S-unknown` and silently bucketed together, which means multiple distinct
|
|
51
|
+
// cycles could appear to share a RED/GREEN pair.
|
|
52
|
+
for (const slice of bySlice.keys()) {
|
|
53
|
+
if (!SLICE_ID_PATTERN.test(slice)) {
|
|
54
|
+
issues.push(`slice "${slice}": id must match /^S-\\d+$/ (e.g. S-1)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
47
57
|
for (const [slice, sliceEntries] of bySlice.entries()) {
|
|
48
58
|
let state = "need_red";
|
|
49
59
|
for (const entry of sliceEntries) {
|
|
@@ -79,7 +89,15 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
79
89
|
state = "green_done";
|
|
80
90
|
continue;
|
|
81
91
|
}
|
|
82
|
-
// refactor
|
|
92
|
+
// refactor — must preserve the passing state established by green.
|
|
93
|
+
if (entry.exitCode === undefined) {
|
|
94
|
+
issues.push(`slice ${slice}: refactor entry must record exitCode 0`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (entry.exitCode !== 0) {
|
|
98
|
+
issues.push(`slice ${slice}: refactor entry exitCode must be 0 (tests must stay green)`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
83
101
|
if (state !== "green_done") {
|
|
84
102
|
issues.push(`slice ${slice}: refactor logged before green`);
|
|
85
103
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -88,6 +88,27 @@ export interface SliceReviewConfig {
|
|
|
88
88
|
/** Tracks on which missed reviews escalate to a doctor warning. */
|
|
89
89
|
enforceOnTracks?: FlowTrack[];
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* File-path routing hints used by workflow-guard during `tdd` stage.
|
|
93
|
+
*
|
|
94
|
+
* - `testPathPatterns`: paths considered test-side changes (RED writes).
|
|
95
|
+
* - `productionPathPatterns`: optional allowlist for production paths that
|
|
96
|
+
* participate in GREEN/REFACTOR checks. When omitted, workflow-guard treats
|
|
97
|
+
* non-test code files as production writes.
|
|
98
|
+
*/
|
|
99
|
+
export interface TddPathConfig {
|
|
100
|
+
testPathPatterns?: string[];
|
|
101
|
+
productionPathPatterns?: string[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Compound-stage clustering policy.
|
|
105
|
+
*
|
|
106
|
+
* `recurrenceThreshold` is the base minimum repeat count for a trigger/action
|
|
107
|
+
* cluster before it is eligible for promotion into durable rules/skills.
|
|
108
|
+
*/
|
|
109
|
+
export interface CompoundConfig {
|
|
110
|
+
recurrenceThreshold?: number;
|
|
111
|
+
}
|
|
91
112
|
export interface VibyConfig {
|
|
92
113
|
version: string;
|
|
93
114
|
flowVersion: string;
|
|
@@ -112,13 +133,20 @@ export interface VibyConfig {
|
|
|
112
133
|
*/
|
|
113
134
|
promptGuardMode?: "advisory" | "strict";
|
|
114
135
|
/**
|
|
115
|
-
* TDD
|
|
136
|
+
* TDD RED -> GREEN -> REFACTOR enforcement mode used by workflow guard hooks.
|
|
116
137
|
*
|
|
117
138
|
* Since v0.43.0 this is an advanced override — see `strictness`.
|
|
118
139
|
*/
|
|
119
140
|
tddEnforcement?: "advisory" | "strict";
|
|
120
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Legacy alias for test-side path detection in workflow-guard.
|
|
143
|
+
* Prefer `tdd.testPathPatterns` in new configs.
|
|
144
|
+
*/
|
|
121
145
|
tddTestGlobs?: string[];
|
|
146
|
+
/** Path-pattern routing for TDD test/production write classification. */
|
|
147
|
+
tdd?: TddPathConfig;
|
|
148
|
+
/** Compound-stage recurrence policy overrides. */
|
|
149
|
+
compound?: CompoundConfig;
|
|
122
150
|
/** When true, cclaw installs managed git pre-commit/pre-push wrappers. */
|
|
123
151
|
gitHookGuards?: boolean;
|
|
124
152
|
/** Default flow track for new runs (quick = shortened path, standard = full pipeline). */
|