cclaw-cli 0.31.0 → 0.33.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 +87 -40
- package/dist/content/archive-command.js +58 -32
- package/dist/content/compound-command.js +79 -24
- package/dist/content/harness-playbooks.d.ts +24 -0
- package/dist/content/harness-playbooks.js +292 -0
- package/dist/content/harnesses-doc.js +13 -3
- package/dist/content/next-command.js +40 -8
- package/dist/content/protocols.js +39 -8
- package/dist/content/retro-command.js +104 -32
- package/dist/content/subagents.js +14 -8
- package/dist/delegation.d.ts +28 -0
- package/dist/delegation.js +47 -7
- package/dist/doctor.js +18 -2
- package/dist/flow-state.d.ts +31 -0
- package/dist/flow-state.js +37 -1
- package/dist/harness-adapters.d.ts +40 -1
- package/dist/harness-adapters.js +24 -5
- package/dist/install.js +36 -1
- package/dist/runs.js +39 -4
- package/package.json +1 -1
|
@@ -43,14 +43,20 @@ Human input remains mandatory only at explicit approval gates (plan approval, us
|
|
|
43
43
|
|
|
44
44
|
### Harness routing
|
|
45
45
|
|
|
46
|
-
| Harness | Delegation tool | Structured ask
|
|
47
|
-
|
|
48
|
-
| Claude | Task
|
|
49
|
-
| Cursor |
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
| Harness | Fallback | Delegation tool | Structured ask | Parity playbook |
|
|
47
|
+
|---|---|---|---|---|
|
|
48
|
+
| Claude | \`native\` | Task (named subagent_type) | AskUserQuestion | \`.cclaw/references/harnesses/claude-playbook.md\` |
|
|
49
|
+
| Cursor | \`generic-dispatch\` | Task (generic subagent_type: explore/generalPurpose/…) | AskQuestion | \`.cclaw/references/harnesses/cursor-playbook.md\` |
|
|
50
|
+
| OpenCode | \`role-switch\` | plugin dispatch _or_ in-session role-switch | plain-text options | \`.cclaw/references/harnesses/opencode-playbook.md\` |
|
|
51
|
+
| Codex | \`role-switch\` | in-session role-switch (mandatory evidenceRefs) | plain-text options | \`.cclaw/references/harnesses/codex-playbook.md\` |
|
|
52
|
+
|
|
53
|
+
**Dispatch rules driven by \`subagentFallback\`:**
|
|
54
|
+
|
|
55
|
+
- \`native\` — use the harness's own named subagent primitive; delegation entry uses \`fulfillmentMode: "isolated"\`.
|
|
56
|
+
- \`generic-dispatch\` — map each cclaw agent onto the generic dispatcher via the harness playbook; delegation entry uses \`fulfillmentMode: "generic-dispatch"\`.
|
|
57
|
+
- \`role-switch\` — announce the role in-session, perform the work, append a delegation row with \`fulfillmentMode: "role-switch"\` and ≥1 \`evidenceRef\`. Without evidenceRefs the \`delegation:mandatory:current_stage\` check reports \`missingEvidence\` and blocks stage completion.
|
|
58
|
+
|
|
59
|
+
The only time a \`harness_limitation\` waiver fires automatically is when every installed harness declares \`subagentFallback: "waiver"\`. cclaw 0.33 no longer maps Codex onto auto-waiver — the agent must role-switch with evidence.
|
|
54
60
|
|
|
55
61
|
### Model routing
|
|
56
62
|
|
package/dist/delegation.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import { type SubagentFallback } from "./harness-adapters.js";
|
|
1
2
|
import type { FlowStage } from "./types.js";
|
|
2
3
|
export type DelegationMode = "mandatory" | "proactive" | "conditional";
|
|
3
4
|
export type DelegationStatus = "scheduled" | "completed" | "failed" | "waived";
|
|
5
|
+
/**
|
|
6
|
+
* How a delegation was actually fulfilled. Advisory — mirrors the harness
|
|
7
|
+
* `subagentFallback` that was in effect when the entry was recorded.
|
|
8
|
+
*
|
|
9
|
+
* - `isolated` — Claude-style isolated subagent worker.
|
|
10
|
+
* - `generic-dispatch` — Cursor-style Task dispatch mapped to a named role.
|
|
11
|
+
* - `role-switch` — performed in-session with explicit role announce.
|
|
12
|
+
* - `harness-waiver` — auto-waived due to missing dispatch capability.
|
|
13
|
+
*/
|
|
14
|
+
export type DelegationFulfillmentMode = "isolated" | "generic-dispatch" | "role-switch" | "harness-waiver";
|
|
4
15
|
export interface DelegationTokenUsage {
|
|
5
16
|
input: number;
|
|
6
17
|
output: number;
|
|
@@ -45,6 +56,12 @@ export type DelegationEntry = {
|
|
|
45
56
|
retryCount?: number;
|
|
46
57
|
/** Optional references to evidence anchors in artifacts. */
|
|
47
58
|
evidenceRefs?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Fulfillment mode this entry was executed under. Omitted on legacy rows
|
|
61
|
+
* (treated as `"isolated"` for Claude, otherwise inferred from the active
|
|
62
|
+
* harness).
|
|
63
|
+
*/
|
|
64
|
+
fulfillmentMode?: DelegationFulfillmentMode;
|
|
48
65
|
/** Schema version marker for span-compatible delegation logs. */
|
|
49
66
|
schemaVersion?: 1;
|
|
50
67
|
};
|
|
@@ -54,10 +71,21 @@ export type DelegationLedger = {
|
|
|
54
71
|
};
|
|
55
72
|
export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
|
|
56
73
|
export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Aggregate the fulfillment mode cclaw expects for the active harness set.
|
|
76
|
+
* Priority native > generic-dispatch > role-switch > waiver — the best
|
|
77
|
+
* available mode wins so mixed installs (e.g. claude + codex) inherit the
|
|
78
|
+
* strongest guarantee.
|
|
79
|
+
*/
|
|
80
|
+
export declare function expectedFulfillmentMode(fallbacks: SubagentFallback[]): DelegationFulfillmentMode;
|
|
57
81
|
export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage): Promise<{
|
|
58
82
|
satisfied: boolean;
|
|
59
83
|
missing: string[];
|
|
60
84
|
waived: string[];
|
|
61
85
|
autoWaived: string[];
|
|
62
86
|
staleIgnored: string[];
|
|
87
|
+
/** Delegation rows missing required evidence under a role-switch fallback. */
|
|
88
|
+
missingEvidence: string[];
|
|
89
|
+
/** Expected fulfillment mode for the active harness set. */
|
|
90
|
+
expectedMode: DelegationFulfillmentMode;
|
|
63
91
|
}>;
|
package/dist/delegation.js
CHANGED
|
@@ -54,6 +54,11 @@ function isDelegationEntry(value) {
|
|
|
54
54
|
(o.taskId === undefined || typeof o.taskId === "string") &&
|
|
55
55
|
(o.waiverReason === undefined || typeof o.waiverReason === "string") &&
|
|
56
56
|
(o.runId === undefined || typeof o.runId === "string") &&
|
|
57
|
+
(o.fulfillmentMode === undefined ||
|
|
58
|
+
o.fulfillmentMode === "isolated" ||
|
|
59
|
+
o.fulfillmentMode === "generic-dispatch" ||
|
|
60
|
+
o.fulfillmentMode === "role-switch" ||
|
|
61
|
+
o.fulfillmentMode === "harness-waiver") &&
|
|
57
62
|
(o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
|
|
58
63
|
(o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
|
|
59
64
|
retryOk &&
|
|
@@ -128,6 +133,23 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
128
133
|
await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`);
|
|
129
134
|
});
|
|
130
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Aggregate the fulfillment mode cclaw expects for the active harness set.
|
|
138
|
+
* Priority native > generic-dispatch > role-switch > waiver — the best
|
|
139
|
+
* available mode wins so mixed installs (e.g. claude + codex) inherit the
|
|
140
|
+
* strongest guarantee.
|
|
141
|
+
*/
|
|
142
|
+
export function expectedFulfillmentMode(fallbacks) {
|
|
143
|
+
if (fallbacks.length === 0)
|
|
144
|
+
return "isolated";
|
|
145
|
+
if (fallbacks.some((f) => f === "native"))
|
|
146
|
+
return "isolated";
|
|
147
|
+
if (fallbacks.some((f) => f === "generic-dispatch"))
|
|
148
|
+
return "generic-dispatch";
|
|
149
|
+
if (fallbacks.some((f) => f === "role-switch"))
|
|
150
|
+
return "role-switch";
|
|
151
|
+
return "harness-waiver";
|
|
152
|
+
}
|
|
131
153
|
export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
132
154
|
const mandatory = stageSchema(stage).mandatoryDelegations;
|
|
133
155
|
const { activeRunId } = await readFlowState(projectRoot);
|
|
@@ -140,15 +162,21 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
|
140
162
|
const missing = [];
|
|
141
163
|
const waived = [];
|
|
142
164
|
const autoWaived = [];
|
|
165
|
+
const missingEvidence = [];
|
|
143
166
|
const config = await readConfig(projectRoot).catch(() => null);
|
|
144
167
|
const harnesses = config?.harnesses ?? [];
|
|
145
|
-
const
|
|
146
|
-
|
|
168
|
+
const fallbacks = harnesses.map((h) => HARNESS_ADAPTERS[h].capabilities.subagentFallback);
|
|
169
|
+
const expectedMode = expectedFulfillmentMode(fallbacks);
|
|
170
|
+
const onlyWaiverFallback = harnesses.length > 0 && fallbacks.every((f) => f === "waiver");
|
|
147
171
|
for (const agent of mandatory) {
|
|
148
172
|
const rows = forRun.filter((e) => e.agent === agent);
|
|
149
|
-
const
|
|
173
|
+
const completedRows = rows.filter((e) => e.status === "completed");
|
|
174
|
+
const waivedRows = rows.filter((e) => e.status === "waived");
|
|
175
|
+
const hasCompleted = completedRows.length > 0;
|
|
176
|
+
const hasWaived = waivedRows.length > 0;
|
|
177
|
+
const ok = hasCompleted || hasWaived;
|
|
150
178
|
if (!ok) {
|
|
151
|
-
if (
|
|
179
|
+
if (onlyWaiverFallback) {
|
|
152
180
|
const existingHarnessWaiver = rows.some((e) => e.status === "waived" && e.waiverReason === "harness_limitation");
|
|
153
181
|
if (!existingHarnessWaiver) {
|
|
154
182
|
await appendDelegation(projectRoot, {
|
|
@@ -157,6 +185,7 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
|
157
185
|
mode: "mandatory",
|
|
158
186
|
status: "waived",
|
|
159
187
|
waiverReason: "harness_limitation",
|
|
188
|
+
fulfillmentMode: "harness-waiver",
|
|
160
189
|
ts: new Date().toISOString(),
|
|
161
190
|
runId: activeRunId
|
|
162
191
|
});
|
|
@@ -167,16 +196,27 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
|
167
196
|
else {
|
|
168
197
|
missing.push(agent);
|
|
169
198
|
}
|
|
199
|
+
continue;
|
|
170
200
|
}
|
|
171
|
-
|
|
201
|
+
if (hasWaived) {
|
|
172
202
|
waived.push(agent);
|
|
173
203
|
}
|
|
204
|
+
// Under role-switch fallback, a `completed` row is only credible if it
|
|
205
|
+
// carries at least one evidenceRef — otherwise the agent might have
|
|
206
|
+
// claimed role-switch satisfaction without showing its work.
|
|
207
|
+
if (hasCompleted &&
|
|
208
|
+
expectedMode === "role-switch" &&
|
|
209
|
+
!completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
|
|
210
|
+
missingEvidence.push(agent);
|
|
211
|
+
}
|
|
174
212
|
}
|
|
175
213
|
return {
|
|
176
|
-
satisfied: missing.length === 0,
|
|
214
|
+
satisfied: missing.length === 0 && missingEvidence.length === 0,
|
|
177
215
|
missing,
|
|
178
216
|
waived,
|
|
179
217
|
autoWaived,
|
|
180
|
-
staleIgnored
|
|
218
|
+
staleIgnored,
|
|
219
|
+
missingEvidence,
|
|
220
|
+
expectedMode
|
|
181
221
|
};
|
|
182
222
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -23,6 +23,7 @@ import { doctorCheckMetadata } from "./doctor-registry.js";
|
|
|
23
23
|
import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
|
|
24
24
|
import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
|
|
25
25
|
import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
|
|
26
|
+
import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName } from "./content/harness-playbooks.js";
|
|
26
27
|
import { validateHookDocument } from "./hook-schema.js";
|
|
27
28
|
const execFileAsync = promisify(execFile);
|
|
28
29
|
async function isGitRepo(projectRoot) {
|
|
@@ -375,6 +376,12 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
375
376
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "references", "harnesses.md")),
|
|
376
377
|
details: `${RUNTIME_ROOT}/references/harnesses.md`
|
|
377
378
|
});
|
|
379
|
+
const playbookDir = path.join(projectRoot, RUNTIME_ROOT, ...HARNESS_PLAYBOOKS_DIR.split("/"));
|
|
380
|
+
checks.push({
|
|
381
|
+
name: "harness_ref:playbooks_index",
|
|
382
|
+
ok: await exists(path.join(playbookDir, "README.md")),
|
|
383
|
+
details: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/README.md`
|
|
384
|
+
});
|
|
378
385
|
const doctorRefDir = path.join(projectRoot, RUNTIME_ROOT, "references", "doctor");
|
|
379
386
|
for (const fileName of Object.keys(DOCTOR_REFERENCE_MARKDOWN)) {
|
|
380
387
|
const refPath = path.join(doctorRefDir, fileName);
|
|
@@ -475,6 +482,12 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
475
482
|
details: shimPath
|
|
476
483
|
});
|
|
477
484
|
}
|
|
485
|
+
const playbookFile = path.join(projectRoot, RUNTIME_ROOT, ...HARNESS_PLAYBOOKS_DIR.split("/"), harnessPlaybookFileName(harness));
|
|
486
|
+
checks.push({
|
|
487
|
+
name: `harness_ref:playbook:${harness}`,
|
|
488
|
+
ok: await exists(playbookFile),
|
|
489
|
+
details: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/${harnessPlaybookFileName(harness)}`
|
|
490
|
+
});
|
|
478
491
|
}
|
|
479
492
|
const agentsFile = path.join(projectRoot, "AGENTS.md");
|
|
480
493
|
let agentsBlockOk = false;
|
|
@@ -1298,12 +1311,15 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1298
1311
|
details: `${RUNTIME_ROOT}/runs must exist for archived feature snapshots`
|
|
1299
1312
|
});
|
|
1300
1313
|
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
|
|
1314
|
+
const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
|
|
1315
|
+
? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
|
|
1316
|
+
: "";
|
|
1301
1317
|
checks.push({
|
|
1302
1318
|
name: "delegation:mandatory:current_stage",
|
|
1303
1319
|
ok: delegation.satisfied,
|
|
1304
1320
|
details: delegation.satisfied
|
|
1305
|
-
? `All mandatory delegations satisfied for stage "${flowState.currentStage}"`
|
|
1306
|
-
: `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}`
|
|
1321
|
+
? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
|
|
1322
|
+
: `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
|
|
1307
1323
|
});
|
|
1308
1324
|
checks.push({
|
|
1309
1325
|
name: "warning:delegation:waived",
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -28,6 +28,35 @@ export interface RetroState {
|
|
|
28
28
|
completedAt?: string;
|
|
29
29
|
compoundEntries: number;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Ship closeout substate machine.
|
|
33
|
+
*
|
|
34
|
+
* After ship completes, cclaw auto-chains retro → compound → archive.
|
|
35
|
+
* Each step is interruptible: `/cc-next` reads `shipSubstate` and resumes
|
|
36
|
+
* from the correct step even across sessions.
|
|
37
|
+
*
|
|
38
|
+
* - `idle` — ship not complete, or closeout not yet started.
|
|
39
|
+
* - `retro_review` — 09-retro.md draft exists; awaiting user edit/accept/skip.
|
|
40
|
+
* - `compound_review` — retro accepted; compound pass awaiting execution
|
|
41
|
+
* (or user skip).
|
|
42
|
+
* - `ready_to_archive` — retro + compound done; archive is the next
|
|
43
|
+
* automatic step.
|
|
44
|
+
* - `archived` — archive completed in this session (transient — archive
|
|
45
|
+
* resets flow-state so this value does not persist between runs).
|
|
46
|
+
*/
|
|
47
|
+
export declare const SHIP_SUBSTATES: readonly ["idle", "retro_review", "compound_review", "ready_to_archive", "archived"];
|
|
48
|
+
export type ShipSubstate = (typeof SHIP_SUBSTATES)[number];
|
|
49
|
+
export interface CloseoutState {
|
|
50
|
+
shipSubstate: ShipSubstate;
|
|
51
|
+
retroDraftedAt?: string;
|
|
52
|
+
retroAcceptedAt?: string;
|
|
53
|
+
retroSkipped?: boolean;
|
|
54
|
+
retroSkipReason?: string;
|
|
55
|
+
compoundCompletedAt?: string;
|
|
56
|
+
compoundSkipped?: boolean;
|
|
57
|
+
compoundPromoted: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function createInitialCloseoutState(): CloseoutState;
|
|
31
60
|
export interface FlowState {
|
|
32
61
|
activeRunId: string;
|
|
33
62
|
currentStage: FlowStage;
|
|
@@ -44,6 +73,8 @@ export interface FlowState {
|
|
|
44
73
|
rewinds: RewindRecord[];
|
|
45
74
|
/** Mandatory retrospective gate status before archive. */
|
|
46
75
|
retro: RetroState;
|
|
76
|
+
/** Ship → retro → compound → archive substate for resumable closeout. */
|
|
77
|
+
closeout: CloseoutState;
|
|
47
78
|
}
|
|
48
79
|
export interface InitialFlowStateOptions {
|
|
49
80
|
activeRunId?: string;
|
package/dist/flow-state.js
CHANGED
|
@@ -2,6 +2,41 @@ import { COMMAND_FILE_ORDER } from "./constants.js";
|
|
|
2
2
|
import { buildTransitionRules, orderedStageSchemas, stageConditionalGateIds, stageGateIds, stageRecommendedGateIds } from "./content/stage-schema.js";
|
|
3
3
|
import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "./types.js";
|
|
4
4
|
export const TRANSITION_RULES = buildTransitionRules();
|
|
5
|
+
/**
|
|
6
|
+
* Ship closeout substate machine.
|
|
7
|
+
*
|
|
8
|
+
* After ship completes, cclaw auto-chains retro → compound → archive.
|
|
9
|
+
* Each step is interruptible: `/cc-next` reads `shipSubstate` and resumes
|
|
10
|
+
* from the correct step even across sessions.
|
|
11
|
+
*
|
|
12
|
+
* - `idle` — ship not complete, or closeout not yet started.
|
|
13
|
+
* - `retro_review` — 09-retro.md draft exists; awaiting user edit/accept/skip.
|
|
14
|
+
* - `compound_review` — retro accepted; compound pass awaiting execution
|
|
15
|
+
* (or user skip).
|
|
16
|
+
* - `ready_to_archive` — retro + compound done; archive is the next
|
|
17
|
+
* automatic step.
|
|
18
|
+
* - `archived` — archive completed in this session (transient — archive
|
|
19
|
+
* resets flow-state so this value does not persist between runs).
|
|
20
|
+
*/
|
|
21
|
+
export const SHIP_SUBSTATES = [
|
|
22
|
+
"idle",
|
|
23
|
+
"retro_review",
|
|
24
|
+
"compound_review",
|
|
25
|
+
"ready_to_archive",
|
|
26
|
+
"archived"
|
|
27
|
+
];
|
|
28
|
+
export function createInitialCloseoutState() {
|
|
29
|
+
return {
|
|
30
|
+
shipSubstate: "idle",
|
|
31
|
+
retroDraftedAt: undefined,
|
|
32
|
+
retroAcceptedAt: undefined,
|
|
33
|
+
retroSkipped: undefined,
|
|
34
|
+
retroSkipReason: undefined,
|
|
35
|
+
compoundCompletedAt: undefined,
|
|
36
|
+
compoundSkipped: undefined,
|
|
37
|
+
compoundPromoted: 0
|
|
38
|
+
};
|
|
39
|
+
}
|
|
5
40
|
export function isFlowTrack(value) {
|
|
6
41
|
return typeof value === "string" && FLOW_TRACKS.includes(value);
|
|
7
42
|
}
|
|
@@ -48,7 +83,8 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
|
|
|
48
83
|
required: false,
|
|
49
84
|
completedAt: undefined,
|
|
50
85
|
compoundEntries: 0
|
|
51
|
-
}
|
|
86
|
+
},
|
|
87
|
+
closeout: createInitialCloseoutState()
|
|
52
88
|
};
|
|
53
89
|
}
|
|
54
90
|
export function canTransition(from, to) {
|
|
@@ -1,19 +1,58 @@
|
|
|
1
1
|
import type { HarnessId } from "./types.js";
|
|
2
2
|
export declare const CCLAW_MARKER_START = "<!-- cclaw-start -->";
|
|
3
3
|
export declare const CCLAW_MARKER_END = "<!-- cclaw-end -->";
|
|
4
|
+
export type SubagentFallback =
|
|
5
|
+
/** Harness has real, isolated subagent dispatch; no fallback needed. */
|
|
6
|
+
"native"
|
|
7
|
+
/**
|
|
8
|
+
* Harness has generic dispatch (e.g. Cursor's Task tool with
|
|
9
|
+
* `subagent_type`) but not user-defined named subagents; cclaw maps each
|
|
10
|
+
* named agent to the generic dispatcher with a structured role prompt.
|
|
11
|
+
*/
|
|
12
|
+
| "generic-dispatch"
|
|
13
|
+
/**
|
|
14
|
+
* No isolated dispatch — the agent performs the named subagent's role
|
|
15
|
+
* in-session with an explicit role announce + delegation-log entry
|
|
16
|
+
* carrying evidenceRefs. Accepted as `completed` in delegation checks.
|
|
17
|
+
*/
|
|
18
|
+
| "role-switch"
|
|
19
|
+
/**
|
|
20
|
+
* No meaningful fallback — mandatory delegations can only be waived
|
|
21
|
+
* under `waiverReason: "harness_limitation"`.
|
|
22
|
+
*/
|
|
23
|
+
| "waiver";
|
|
4
24
|
export interface HarnessAdapter {
|
|
5
25
|
id: HarnessId;
|
|
6
26
|
commandDir: string;
|
|
7
27
|
capabilities: {
|
|
8
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Level of native subagent dispatch:
|
|
30
|
+
* - `full` — isolated workers + user-defined named subagents (Claude).
|
|
31
|
+
* - `generic` — generic dispatcher (Task) without named agents (Cursor).
|
|
32
|
+
* - `partial` — plugin-based dispatch, not a first-class primitive
|
|
33
|
+
* (OpenCode).
|
|
34
|
+
* - `none` — no dispatch primitive at all (Codex).
|
|
35
|
+
*/
|
|
36
|
+
nativeSubagentDispatch: "full" | "generic" | "partial" | "none";
|
|
9
37
|
hookSurface: "full" | "plugin" | "limited" | "none";
|
|
10
38
|
structuredAsk: "AskUserQuestion" | "AskQuestion" | "plain-text";
|
|
39
|
+
/**
|
|
40
|
+
* Declared fallback pattern used when the harness cannot satisfy a
|
|
41
|
+
* mandatory delegation natively. Drives `checkMandatoryDelegations`
|
|
42
|
+
* and the generated playbook per harness.
|
|
43
|
+
*/
|
|
44
|
+
subagentFallback: SubagentFallback;
|
|
11
45
|
};
|
|
12
46
|
}
|
|
13
47
|
export declare function harnessShimFileNames(): string[];
|
|
14
48
|
export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
|
|
15
49
|
export type HarnessTier = "tier1" | "tier2" | "tier3";
|
|
16
50
|
export declare function harnessTier(harnessId: HarnessId): HarnessTier;
|
|
51
|
+
/**
|
|
52
|
+
* Harness IDs ordered from best (tier1) to least-capable. Stable sort — same
|
|
53
|
+
* tier preserves declaration order.
|
|
54
|
+
*/
|
|
55
|
+
export declare function harnessesByTier(): HarnessId[];
|
|
17
56
|
/** Removes the cclaw AGENTS.md block. */
|
|
18
57
|
export declare function stripCclawBlock(content: string): string;
|
|
19
58
|
export declare function removeCclawFromAgentsMd(projectRoot: string): Promise<void>;
|
package/dist/harness-adapters.js
CHANGED
|
@@ -54,16 +54,22 @@ export const HARNESS_ADAPTERS = {
|
|
|
54
54
|
capabilities: {
|
|
55
55
|
nativeSubagentDispatch: "full",
|
|
56
56
|
hookSurface: "full",
|
|
57
|
-
structuredAsk: "AskUserQuestion"
|
|
57
|
+
structuredAsk: "AskUserQuestion",
|
|
58
|
+
subagentFallback: "native"
|
|
58
59
|
}
|
|
59
60
|
},
|
|
60
61
|
cursor: {
|
|
61
62
|
id: "cursor",
|
|
62
63
|
commandDir: ".cursor/commands",
|
|
63
64
|
capabilities: {
|
|
64
|
-
|
|
65
|
+
// Cursor has a real Task tool with subagent_type (generalPurpose,
|
|
66
|
+
// explore, shell, browser-use, …) but no user-defined named
|
|
67
|
+
// subagents. cclaw maps each named agent (planner/reviewer/…) onto
|
|
68
|
+
// generic dispatch with a role prompt — see the cursor playbook.
|
|
69
|
+
nativeSubagentDispatch: "generic",
|
|
65
70
|
hookSurface: "full",
|
|
66
|
-
structuredAsk: "AskQuestion"
|
|
71
|
+
structuredAsk: "AskQuestion",
|
|
72
|
+
subagentFallback: "generic-dispatch"
|
|
67
73
|
}
|
|
68
74
|
},
|
|
69
75
|
opencode: {
|
|
@@ -72,7 +78,8 @@ export const HARNESS_ADAPTERS = {
|
|
|
72
78
|
capabilities: {
|
|
73
79
|
nativeSubagentDispatch: "partial",
|
|
74
80
|
hookSurface: "plugin",
|
|
75
|
-
structuredAsk: "plain-text"
|
|
81
|
+
structuredAsk: "plain-text",
|
|
82
|
+
subagentFallback: "role-switch"
|
|
76
83
|
}
|
|
77
84
|
},
|
|
78
85
|
codex: {
|
|
@@ -81,7 +88,8 @@ export const HARNESS_ADAPTERS = {
|
|
|
81
88
|
capabilities: {
|
|
82
89
|
nativeSubagentDispatch: "none",
|
|
83
90
|
hookSurface: "full",
|
|
84
|
-
structuredAsk: "plain-text"
|
|
91
|
+
structuredAsk: "plain-text",
|
|
92
|
+
subagentFallback: "role-switch"
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
};
|
|
@@ -94,11 +102,22 @@ export function harnessTier(harnessId) {
|
|
|
94
102
|
}
|
|
95
103
|
if (capabilities.hookSurface === "full" ||
|
|
96
104
|
capabilities.hookSurface === "plugin" ||
|
|
105
|
+
capabilities.nativeSubagentDispatch === "generic" ||
|
|
97
106
|
capabilities.nativeSubagentDispatch === "partial") {
|
|
98
107
|
return "tier2";
|
|
99
108
|
}
|
|
100
109
|
return "tier3";
|
|
101
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Harness IDs ordered from best (tier1) to least-capable. Stable sort — same
|
|
113
|
+
* tier preserves declaration order.
|
|
114
|
+
*/
|
|
115
|
+
export function harnessesByTier() {
|
|
116
|
+
return Object.keys(HARNESS_ADAPTERS).sort((a, b) => {
|
|
117
|
+
const tierOrder = { tier1: 0, tier2: 1, tier3: 2 };
|
|
118
|
+
return tierOrder[harnessTier(a)] - tierOrder[harnessTier(b)];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
102
121
|
function agentsMdBlock() {
|
|
103
122
|
return `${CCLAW_MARKER_START}
|
|
104
123
|
## Cclaw — Workflow Adapter
|
package/dist/install.js
CHANGED
|
@@ -37,6 +37,7 @@ import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
|
|
|
37
37
|
import { HARNESS_TOOL_REFS_DIR, HARNESS_TOOL_REFS_INDEX_MD, harnessToolRefMarkdown } from "./content/harness-tool-refs.js";
|
|
38
38
|
import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
|
|
39
39
|
import { harnessIntegrationDocMarkdown } from "./content/harnesses-doc.js";
|
|
40
|
+
import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName, harnessPlaybookMarkdown, harnessPlaybooksIndexMarkdown } from "./content/harness-playbooks.js";
|
|
40
41
|
import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./content/hook-events.js";
|
|
41
42
|
import { createInitialFlowState } from "./flow-state.js";
|
|
42
43
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
@@ -293,6 +294,15 @@ async function writeSkills(projectRoot, config) {
|
|
|
293
294
|
await writeFileSafe(runtimePath(projectRoot, ...doctorRefsDir, fileName), markdown);
|
|
294
295
|
}
|
|
295
296
|
await writeFileSafe(runtimePath(projectRoot, "references", "harnesses.md"), harnessIntegrationDocMarkdown());
|
|
297
|
+
// Per-harness parity playbooks. Generated for every supported harness
|
|
298
|
+
// regardless of which harnesses the project installed — the index always
|
|
299
|
+
// resolves, and doctor only asserts presence of the installed harnesses'
|
|
300
|
+
// playbooks (see runtime-integrity checks).
|
|
301
|
+
const playbookDirSegments = HARNESS_PLAYBOOKS_DIR.split("/");
|
|
302
|
+
await writeFileSafe(runtimePath(projectRoot, ...playbookDirSegments, "README.md"), harnessPlaybooksIndexMarkdown());
|
|
303
|
+
for (const harness of harnessIds) {
|
|
304
|
+
await writeFileSafe(runtimePath(projectRoot, ...playbookDirSegments, harnessPlaybookFileName(harness)), harnessPlaybookMarkdown(harness));
|
|
305
|
+
}
|
|
296
306
|
}
|
|
297
307
|
async function writeUtilityCommands(projectRoot) {
|
|
298
308
|
await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
|
|
@@ -948,15 +958,40 @@ async function writeHarnessGapsState(projectRoot, harnesses) {
|
|
|
948
958
|
if (capabilities.structuredAsk === "plain-text") {
|
|
949
959
|
missingCapabilities.push("structuredAsk:none");
|
|
950
960
|
}
|
|
961
|
+
const remediation = [];
|
|
962
|
+
switch (capabilities.subagentFallback) {
|
|
963
|
+
case "native":
|
|
964
|
+
// nothing to remediate — harness has first-class dispatch
|
|
965
|
+
break;
|
|
966
|
+
case "generic-dispatch":
|
|
967
|
+
remediation.push(`subagent dispatch → map named cclaw agents onto generic Task subagent_type per ${HARNESS_PLAYBOOKS_DIR}/${harness}-playbook.md`);
|
|
968
|
+
break;
|
|
969
|
+
case "role-switch":
|
|
970
|
+
remediation.push(`subagent dispatch → role-switch in-session with evidenceRefs per ${HARNESS_PLAYBOOKS_DIR}/${harness}-playbook.md`);
|
|
971
|
+
break;
|
|
972
|
+
case "waiver":
|
|
973
|
+
remediation.push(`subagent dispatch → record explicit harness_limitation waiver; no parity path available`);
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
if (capabilities.structuredAsk === "plain-text") {
|
|
977
|
+
remediation.push("structured ask → fall back to a numbered plain-text list; first option is default");
|
|
978
|
+
}
|
|
979
|
+
for (const event of missingHookEvents) {
|
|
980
|
+
remediation.push(`hook event ${event} → schedule the corresponding script manually or accept reduced observability`);
|
|
981
|
+
}
|
|
951
982
|
return {
|
|
952
983
|
harness,
|
|
953
984
|
tier: harnessTier(harness),
|
|
985
|
+
subagentFallback: capabilities.subagentFallback,
|
|
986
|
+
playbookPath: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/${harness}-playbook.md`,
|
|
954
987
|
missingCapabilities,
|
|
955
|
-
missingHookEvents
|
|
988
|
+
missingHookEvents,
|
|
989
|
+
remediation
|
|
956
990
|
};
|
|
957
991
|
});
|
|
958
992
|
await writeFileSafe(runtimePath(projectRoot, "state", "harness-gaps.json"), `${JSON.stringify({
|
|
959
993
|
generatedAt: new Date().toISOString(),
|
|
994
|
+
schemaVersion: 2,
|
|
960
995
|
harnesses: report
|
|
961
996
|
}, null, 2)}\n`);
|
|
962
997
|
}
|
package/dist/runs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { COMMAND_FILE_ORDER, RUNTIME_ROOT } from "./constants.js";
|
|
4
|
-
import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack } from "./flow-state.js";
|
|
4
|
+
import { canTransition, createInitialCloseoutState, createInitialFlowState, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
|
|
5
5
|
import { ensureFeatureSystem, readActiveFeature, syncActiveFeatureSnapshot } from "./feature-system.js";
|
|
6
6
|
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
7
7
|
export class InvalidStageTransitionError extends Error {
|
|
@@ -272,6 +272,37 @@ function sanitizeRetroState(value) {
|
|
|
272
272
|
compoundEntries
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
+
function isShipSubstate(value) {
|
|
276
|
+
return typeof value === "string" && SHIP_SUBSTATES.includes(value);
|
|
277
|
+
}
|
|
278
|
+
function sanitizeCloseoutState(value) {
|
|
279
|
+
const fallback = createInitialCloseoutState();
|
|
280
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
281
|
+
return fallback;
|
|
282
|
+
}
|
|
283
|
+
const typed = value;
|
|
284
|
+
const shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
|
|
285
|
+
const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
|
|
286
|
+
const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
|
|
287
|
+
const retroSkipped = typeof typed.retroSkipped === "boolean" ? typed.retroSkipped : undefined;
|
|
288
|
+
const retroSkipReason = typeof typed.retroSkipReason === "string" ? typed.retroSkipReason : undefined;
|
|
289
|
+
const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
|
|
290
|
+
const compoundSkipped = typeof typed.compoundSkipped === "boolean" ? typed.compoundSkipped : undefined;
|
|
291
|
+
const promotedRaw = typed.compoundPromoted;
|
|
292
|
+
const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
|
|
293
|
+
? Math.floor(promotedRaw)
|
|
294
|
+
: 0;
|
|
295
|
+
return {
|
|
296
|
+
shipSubstate,
|
|
297
|
+
retroDraftedAt,
|
|
298
|
+
retroAcceptedAt,
|
|
299
|
+
retroSkipped,
|
|
300
|
+
retroSkipReason,
|
|
301
|
+
compoundCompletedAt,
|
|
302
|
+
compoundSkipped,
|
|
303
|
+
compoundPromoted
|
|
304
|
+
};
|
|
305
|
+
}
|
|
275
306
|
function coerceFlowState(parsed) {
|
|
276
307
|
const track = coerceTrack(parsed.track);
|
|
277
308
|
const next = createInitialFlowState("active", track);
|
|
@@ -289,7 +320,8 @@ function coerceFlowState(parsed) {
|
|
|
289
320
|
skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
|
|
290
321
|
staleStages: sanitizeStaleStages(parsed.staleStages),
|
|
291
322
|
rewinds: sanitizeRewinds(parsed.rewinds),
|
|
292
|
-
retro: sanitizeRetroState(parsed.retro)
|
|
323
|
+
retro: sanitizeRetroState(parsed.retro),
|
|
324
|
+
closeout: sanitizeCloseoutState(parsed.closeout)
|
|
293
325
|
};
|
|
294
326
|
}
|
|
295
327
|
function toArchiveDate(date = new Date()) {
|
|
@@ -536,9 +568,12 @@ export async function archiveRun(projectRoot, featureName, options = {}) {
|
|
|
536
568
|
if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
|
|
537
569
|
throw new Error("archive --skip-retro requires --retro-reason=<text>.");
|
|
538
570
|
}
|
|
539
|
-
|
|
571
|
+
const retroSkippedInCloseout = sourceState.closeout.retroSkipped === true &&
|
|
572
|
+
typeof sourceState.closeout.retroSkipReason === "string" &&
|
|
573
|
+
sourceState.closeout.retroSkipReason.trim().length > 0;
|
|
574
|
+
if (retroGate.required && !retroGate.completed && !skipRetro && !retroSkippedInCloseout) {
|
|
540
575
|
throw new Error("Archive blocked: retro gate is required after ship completion. " +
|
|
541
|
-
"Run /cc-
|
|
576
|
+
"Run /cc-next (auto-runs retro) or, for CLI-only flows, re-run `cclaw archive --skip-retro --retro-reason=<text>`.");
|
|
542
577
|
}
|
|
543
578
|
if (retroGate.completed) {
|
|
544
579
|
const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
|