cclaw-cli 6.6.0 → 6.8.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/dist/artifact-linter/findings-dedup.d.ts +56 -0
- package/dist/artifact-linter/findings-dedup.js +232 -0
- package/dist/artifact-linter/plan.js +3 -2
- package/dist/artifact-linter/shared.d.ts +49 -0
- package/dist/artifact-linter/shared.js +35 -0
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/artifact-linter.js +45 -3
- package/dist/content/hooks.js +241 -7
- package/dist/content/node-hooks.js +43 -0
- package/dist/content/skills-elicitation.js +3 -6
- package/dist/content/skills.js +3 -1
- package/dist/content/stages/brainstorm.js +4 -4
- package/dist/content/stages/scope.js +2 -2
- package/dist/content/templates.js +3 -2
- package/dist/delegation.d.ts +107 -0
- package/dist/delegation.js +223 -6
- package/dist/internal/advance-stage/advance.js +23 -1
- package/dist/internal/advance-stage/parsers.d.ts +8 -0
- package/dist/internal/advance-stage/parsers.js +7 -0
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +3 -0
- package/dist/internal/advance-stage/proactive-delegation-trace.js +8 -1
- package/dist/internal/advance-stage/rewind.js +2 -2
- package/dist/internal/advance-stage/start-flow.js +4 -1
- package/dist/internal/advance-stage.js +41 -2
- package/dist/internal/flow-state-repair.d.ts +13 -0
- package/dist/internal/flow-state-repair.js +65 -0
- package/dist/internal/waiver-grant.d.ts +62 -0
- package/dist/internal/waiver-grant.js +294 -0
- package/dist/run-persistence.d.ts +70 -0
- package/dist/run-persistence.js +215 -3
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -1
- package/dist/runtime/run-hook.mjs +43 -0
- package/package.json +1 -1
package/dist/delegation.d.ts
CHANGED
|
@@ -60,6 +60,15 @@ export type DelegationEntry = {
|
|
|
60
60
|
taskId?: string;
|
|
61
61
|
waiverReason?: string;
|
|
62
62
|
acceptedBy?: DelegationWaiverAcceptedBy;
|
|
63
|
+
/**
|
|
64
|
+
* Waiver approval token captured from `cclaw-cli internal waiver-grant`.
|
|
65
|
+
* Present on waiver rows written after v6.7.0. Legacy waiver rows omit
|
|
66
|
+
* these fields and are surfaced as the advisory linter finding
|
|
67
|
+
* `waiver_legacy_provenance`.
|
|
68
|
+
*/
|
|
69
|
+
approvalToken?: string;
|
|
70
|
+
approvalReason?: string;
|
|
71
|
+
approvalIssuedAt?: string;
|
|
63
72
|
ts?: string;
|
|
64
73
|
/**
|
|
65
74
|
* Run id the entry belongs to. Older ledgers written before 0.5.17 may omit this;
|
|
@@ -107,6 +116,19 @@ export type DelegationEntry = {
|
|
|
107
116
|
* `dispatchSurface`, `agentDefinitionPath`, and ACK timestamp
|
|
108
117
|
*/
|
|
109
118
|
schemaVersion?: 1 | 2 | 3;
|
|
119
|
+
/**
|
|
120
|
+
* v6.8.0 — when set, the operator explicitly opted into running this
|
|
121
|
+
* scheduled span concurrently with another active span on the same
|
|
122
|
+
* `(stage, agent)` pair. Bypasses the dispatch-dedup check.
|
|
123
|
+
*/
|
|
124
|
+
allowParallel?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* v6.8.0 — set on synthetic terminal `stale` rows written via
|
|
127
|
+
* `--supersede=<prevSpanId>`. References the new spanId that
|
|
128
|
+
* superseded this span. Helps `/cc tree` and the linter report a
|
|
129
|
+
* coherent successor chain.
|
|
130
|
+
*/
|
|
131
|
+
supersededBy?: string;
|
|
110
132
|
};
|
|
111
133
|
export declare const DELEGATION_LEDGER_SCHEMA_VERSION: 3;
|
|
112
134
|
export type DelegationLedger = {
|
|
@@ -135,6 +157,91 @@ export declare function readDelegationEvents(projectRoot: string): Promise<{
|
|
|
135
157
|
events: DelegationEvent[];
|
|
136
158
|
corruptLines: number[];
|
|
137
159
|
}>;
|
|
160
|
+
/**
|
|
161
|
+
* Fold ledger entries to the latest row per `spanId` and keep only spans
|
|
162
|
+
* whose latest status is still active (`scheduled | launched |
|
|
163
|
+
* acknowledged`). Used by the `state/subagents.json` writer so the
|
|
164
|
+
* tracker never reports a span that already has a terminal row.
|
|
165
|
+
*
|
|
166
|
+
* Output is ordered by ascending `startTs ?? ts` so existing UI
|
|
167
|
+
* consumers see a stable presentation order.
|
|
168
|
+
*
|
|
169
|
+
* Rows without a `spanId` are skipped — they are not addressable by
|
|
170
|
+
* the tracker contract and would collide on the empty key.
|
|
171
|
+
*
|
|
172
|
+
* Callers are expected to pass entries already filtered to the active
|
|
173
|
+
* `runId`; cross-run rows are therefore not re-filtered here.
|
|
174
|
+
*
|
|
175
|
+
* keep in sync with the inline copy in
|
|
176
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
177
|
+
*/
|
|
178
|
+
export declare function computeActiveSubagents(entries: DelegationEntry[]): DelegationEntry[];
|
|
179
|
+
/**
|
|
180
|
+
* v6.8.0 — thrown by `validateMonotonicTimestamps` when an incoming row
|
|
181
|
+
* would push a span's timeline backwards. Carries enough context that
|
|
182
|
+
* the CLI / hook surface can format a `delegation_timestamp_non_monotonic`
|
|
183
|
+
* JSON payload without re-deriving the offending field.
|
|
184
|
+
*
|
|
185
|
+
* keep in sync with the inline copy in
|
|
186
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
187
|
+
*/
|
|
188
|
+
export declare class DelegationTimestampError extends Error {
|
|
189
|
+
readonly field: string;
|
|
190
|
+
readonly actual: string;
|
|
191
|
+
readonly priorBound: string;
|
|
192
|
+
constructor(field: string, actual: string, priorBound: string);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* v6.8.0 — enforce that lifecycle timestamps on a delegation span move
|
|
196
|
+
* forward (or stay equal). Validates both per-row invariants
|
|
197
|
+
* (`startTs ≤ launchedTs ≤ ackTs ≤ completedTs`) and a cross-row
|
|
198
|
+
* invariant: the union of prior rows for this `spanId` plus the
|
|
199
|
+
* incoming row must have non-decreasing `ts`.
|
|
200
|
+
*
|
|
201
|
+
* Equality is allowed because fast-completing dispatches legitimately
|
|
202
|
+
* collapse multiple lifecycle markers onto the same instant.
|
|
203
|
+
*
|
|
204
|
+
* keep in sync with the inline copy in
|
|
205
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
206
|
+
*/
|
|
207
|
+
export declare function validateMonotonicTimestamps(stamped: DelegationEntry, prior: DelegationEntry[]): void;
|
|
208
|
+
/**
|
|
209
|
+
* v6.8.0 — thrown by `appendDelegation` when the operator opens a
|
|
210
|
+
* second `scheduled` span on the same `(stage, agent)` pair while an
|
|
211
|
+
* earlier span on the same pair is still active. Callers can catch and
|
|
212
|
+
* either pass the existing span id via `--supersede=<id>` (which
|
|
213
|
+
* pre-writes a synthetic `stale` row) or `--allow-parallel` to record
|
|
214
|
+
* concurrent spans intentionally.
|
|
215
|
+
*/
|
|
216
|
+
export declare class DispatchDuplicateError extends Error {
|
|
217
|
+
readonly existingSpanId: string;
|
|
218
|
+
readonly existingStatus: DelegationStatus;
|
|
219
|
+
readonly newSpanId: string;
|
|
220
|
+
readonly pair: {
|
|
221
|
+
stage: string;
|
|
222
|
+
agent: string;
|
|
223
|
+
};
|
|
224
|
+
constructor(params: {
|
|
225
|
+
existingSpanId: string;
|
|
226
|
+
existingStatus: DelegationStatus;
|
|
227
|
+
newSpanId: string;
|
|
228
|
+
pair: {
|
|
229
|
+
stage: string;
|
|
230
|
+
agent: string;
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* v6.8.0 — find the latest active span for a given `(stage, agent)`
|
|
236
|
+
* pair in the supplied ledger entries. Returns the row whose latest
|
|
237
|
+
* status (after the latest-by-spanId fold) is still in the active set
|
|
238
|
+
* (`scheduled | launched | acknowledged`). Caller is responsible for
|
|
239
|
+
* filtering to the current run.
|
|
240
|
+
*
|
|
241
|
+
* keep in sync with the inline copy in
|
|
242
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
243
|
+
*/
|
|
244
|
+
export declare function findActiveSpanForPair(stage: string, agent: string, runId: string, ledger: DelegationLedger): DelegationEntry | null;
|
|
138
245
|
export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
|
|
139
246
|
/**
|
|
140
247
|
* Aggregate the fulfillment mode cclaw expects for the active harness set.
|
package/dist/delegation.js
CHANGED
|
@@ -199,6 +199,9 @@ function isDelegationEntry(value) {
|
|
|
199
199
|
(o.taskId === undefined || typeof o.taskId === "string") &&
|
|
200
200
|
(o.waiverReason === undefined || typeof o.waiverReason === "string") &&
|
|
201
201
|
(o.acceptedBy === undefined || o.acceptedBy === "user-flag") &&
|
|
202
|
+
(o.approvalToken === undefined || typeof o.approvalToken === "string") &&
|
|
203
|
+
(o.approvalReason === undefined || typeof o.approvalReason === "string") &&
|
|
204
|
+
(o.approvalIssuedAt === undefined || typeof o.approvalIssuedAt === "string") &&
|
|
202
205
|
waiverOk &&
|
|
203
206
|
(o.runId === undefined || typeof o.runId === "string") &&
|
|
204
207
|
(o.fulfillmentMode === undefined ||
|
|
@@ -219,7 +222,9 @@ function isDelegationEntry(value) {
|
|
|
219
222
|
retryOk &&
|
|
220
223
|
(o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
|
|
221
224
|
(o.skill === undefined || typeof o.skill === "string") &&
|
|
222
|
-
(o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3)
|
|
225
|
+
(o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3) &&
|
|
226
|
+
(o.allowParallel === undefined || typeof o.allowParallel === "boolean") &&
|
|
227
|
+
(o.supersededBy === undefined || typeof o.supersededBy === "string"));
|
|
223
228
|
}
|
|
224
229
|
function isDelegationDispatchSurface(value) {
|
|
225
230
|
return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
|
|
@@ -375,10 +380,196 @@ async function appendDelegationEvent(projectRoot, event) {
|
|
|
375
380
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
376
381
|
await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
377
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Effective timestamp used to order rows that share a `spanId`. Newest
|
|
385
|
+
* lifecycle column wins. Returns the empty string when nothing is set
|
|
386
|
+
* so the caller still has a stable lexicographic compare key.
|
|
387
|
+
*
|
|
388
|
+
* keep in sync with the inline copy in
|
|
389
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
390
|
+
*/
|
|
391
|
+
function effectiveSpanTs(entry) {
|
|
392
|
+
return entry.completedTs ?? entry.ackTs ?? entry.launchedTs ?? entry.endTs ?? entry.startTs ?? entry.ts ?? "";
|
|
393
|
+
}
|
|
394
|
+
const ACTIVE_DELEGATION_STATUSES = new Set([
|
|
395
|
+
"scheduled",
|
|
396
|
+
"launched",
|
|
397
|
+
"acknowledged"
|
|
398
|
+
]);
|
|
399
|
+
/**
|
|
400
|
+
* Fold ledger entries to the latest row per `spanId` and keep only spans
|
|
401
|
+
* whose latest status is still active (`scheduled | launched |
|
|
402
|
+
* acknowledged`). Used by the `state/subagents.json` writer so the
|
|
403
|
+
* tracker never reports a span that already has a terminal row.
|
|
404
|
+
*
|
|
405
|
+
* Output is ordered by ascending `startTs ?? ts` so existing UI
|
|
406
|
+
* consumers see a stable presentation order.
|
|
407
|
+
*
|
|
408
|
+
* Rows without a `spanId` are skipped — they are not addressable by
|
|
409
|
+
* the tracker contract and would collide on the empty key.
|
|
410
|
+
*
|
|
411
|
+
* Callers are expected to pass entries already filtered to the active
|
|
412
|
+
* `runId`; cross-run rows are therefore not re-filtered here.
|
|
413
|
+
*
|
|
414
|
+
* keep in sync with the inline copy in
|
|
415
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
416
|
+
*/
|
|
417
|
+
export function computeActiveSubagents(entries) {
|
|
418
|
+
const latestBySpan = new Map();
|
|
419
|
+
for (const entry of entries) {
|
|
420
|
+
if (!entry.spanId)
|
|
421
|
+
continue;
|
|
422
|
+
const existing = latestBySpan.get(entry.spanId);
|
|
423
|
+
if (!existing) {
|
|
424
|
+
latestBySpan.set(entry.spanId, entry);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const existingTs = effectiveSpanTs(existing);
|
|
428
|
+
const incomingTs = effectiveSpanTs(entry);
|
|
429
|
+
if (incomingTs >= existingTs) {
|
|
430
|
+
latestBySpan.set(entry.spanId, entry);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const folded = [];
|
|
434
|
+
for (const entry of latestBySpan.values()) {
|
|
435
|
+
if (ACTIVE_DELEGATION_STATUSES.has(entry.status)) {
|
|
436
|
+
folded.push(entry);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
folded.sort((a, b) => {
|
|
440
|
+
const aKey = a.startTs ?? a.ts ?? "";
|
|
441
|
+
const bKey = b.startTs ?? b.ts ?? "";
|
|
442
|
+
if (aKey === bKey)
|
|
443
|
+
return 0;
|
|
444
|
+
return aKey < bKey ? -1 : 1;
|
|
445
|
+
});
|
|
446
|
+
return folded;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* v6.8.0 — thrown by `validateMonotonicTimestamps` when an incoming row
|
|
450
|
+
* would push a span's timeline backwards. Carries enough context that
|
|
451
|
+
* the CLI / hook surface can format a `delegation_timestamp_non_monotonic`
|
|
452
|
+
* JSON payload without re-deriving the offending field.
|
|
453
|
+
*
|
|
454
|
+
* keep in sync with the inline copy in
|
|
455
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
456
|
+
*/
|
|
457
|
+
export class DelegationTimestampError extends Error {
|
|
458
|
+
field;
|
|
459
|
+
actual;
|
|
460
|
+
priorBound;
|
|
461
|
+
constructor(field, actual, priorBound) {
|
|
462
|
+
super(`delegation_timestamp_non_monotonic — ${field}: ${actual} < ${priorBound}`);
|
|
463
|
+
this.name = "DelegationTimestampError";
|
|
464
|
+
this.field = field;
|
|
465
|
+
this.actual = actual;
|
|
466
|
+
this.priorBound = priorBound;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* v6.8.0 — enforce that lifecycle timestamps on a delegation span move
|
|
471
|
+
* forward (or stay equal). Validates both per-row invariants
|
|
472
|
+
* (`startTs ≤ launchedTs ≤ ackTs ≤ completedTs`) and a cross-row
|
|
473
|
+
* invariant: the union of prior rows for this `spanId` plus the
|
|
474
|
+
* incoming row must have non-decreasing `ts`.
|
|
475
|
+
*
|
|
476
|
+
* Equality is allowed because fast-completing dispatches legitimately
|
|
477
|
+
* collapse multiple lifecycle markers onto the same instant.
|
|
478
|
+
*
|
|
479
|
+
* keep in sync with the inline copy in
|
|
480
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
481
|
+
*/
|
|
482
|
+
export function validateMonotonicTimestamps(stamped, prior) {
|
|
483
|
+
const startTs = stamped.startTs;
|
|
484
|
+
if (stamped.launchedTs && startTs && stamped.launchedTs < startTs) {
|
|
485
|
+
throw new DelegationTimestampError("launchedTs", stamped.launchedTs, startTs);
|
|
486
|
+
}
|
|
487
|
+
if (stamped.ackTs) {
|
|
488
|
+
const ackBound = stamped.launchedTs ?? startTs;
|
|
489
|
+
if (ackBound && stamped.ackTs < ackBound) {
|
|
490
|
+
throw new DelegationTimestampError("ackTs", stamped.ackTs, ackBound);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (stamped.completedTs) {
|
|
494
|
+
const completedBound = stamped.ackTs ?? stamped.launchedTs ?? startTs;
|
|
495
|
+
if (completedBound && stamped.completedTs < completedBound) {
|
|
496
|
+
throw new DelegationTimestampError("completedTs", stamped.completedTs, completedBound);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (!stamped.spanId)
|
|
500
|
+
return;
|
|
501
|
+
const priorForSpan = prior.filter((entry) => entry.spanId === stamped.spanId);
|
|
502
|
+
if (priorForSpan.length === 0)
|
|
503
|
+
return;
|
|
504
|
+
const timeline = [...priorForSpan, stamped]
|
|
505
|
+
.map((entry) => ({ entry, ts: entry.ts ?? entry.startTs ?? "" }))
|
|
506
|
+
.filter((row) => row.ts.length > 0)
|
|
507
|
+
.sort((a, b) => (a.ts === b.ts ? 0 : a.ts < b.ts ? -1 : 1));
|
|
508
|
+
for (let i = 1; i < timeline.length; i += 1) {
|
|
509
|
+
const previous = timeline[i - 1];
|
|
510
|
+
const current = timeline[i];
|
|
511
|
+
if (current.ts < previous.ts) {
|
|
512
|
+
throw new DelegationTimestampError("ts", current.ts, previous.ts);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Find the latest existing row by `ts` for the same spanId; if the
|
|
516
|
+
// new row's `ts` is older than that latest, the timeline regressed.
|
|
517
|
+
const latestPrior = priorForSpan
|
|
518
|
+
.map((entry) => entry.ts ?? entry.startTs ?? "")
|
|
519
|
+
.filter((ts) => ts.length > 0)
|
|
520
|
+
.sort()
|
|
521
|
+
.at(-1);
|
|
522
|
+
const stampedTs = stamped.ts ?? stamped.startTs ?? "";
|
|
523
|
+
if (latestPrior && stampedTs && stampedTs < latestPrior) {
|
|
524
|
+
throw new DelegationTimestampError("ts", stampedTs, latestPrior);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* v6.8.0 — thrown by `appendDelegation` when the operator opens a
|
|
529
|
+
* second `scheduled` span on the same `(stage, agent)` pair while an
|
|
530
|
+
* earlier span on the same pair is still active. Callers can catch and
|
|
531
|
+
* either pass the existing span id via `--supersede=<id>` (which
|
|
532
|
+
* pre-writes a synthetic `stale` row) or `--allow-parallel` to record
|
|
533
|
+
* concurrent spans intentionally.
|
|
534
|
+
*/
|
|
535
|
+
export class DispatchDuplicateError extends Error {
|
|
536
|
+
existingSpanId;
|
|
537
|
+
existingStatus;
|
|
538
|
+
newSpanId;
|
|
539
|
+
pair;
|
|
540
|
+
constructor(params) {
|
|
541
|
+
super(`dispatch_duplicate — already-active spanId=${params.existingSpanId} (status=${params.existingStatus}) on stage=${params.pair.stage}, agent=${params.pair.agent}. ` +
|
|
542
|
+
`pass --supersede=${params.existingSpanId} to close the previous span as stale, or --allow-parallel to record both as concurrent.`);
|
|
543
|
+
this.name = "DispatchDuplicateError";
|
|
544
|
+
this.existingSpanId = params.existingSpanId;
|
|
545
|
+
this.existingStatus = params.existingStatus;
|
|
546
|
+
this.newSpanId = params.newSpanId;
|
|
547
|
+
this.pair = params.pair;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* v6.8.0 — find the latest active span for a given `(stage, agent)`
|
|
552
|
+
* pair in the supplied ledger entries. Returns the row whose latest
|
|
553
|
+
* status (after the latest-by-spanId fold) is still in the active set
|
|
554
|
+
* (`scheduled | launched | acknowledged`). Caller is responsible for
|
|
555
|
+
* filtering to the current run.
|
|
556
|
+
*
|
|
557
|
+
* keep in sync with the inline copy in
|
|
558
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
559
|
+
*/
|
|
560
|
+
export function findActiveSpanForPair(stage, agent, runId, ledger) {
|
|
561
|
+
const sameRun = ledger.entries.filter((entry) => {
|
|
562
|
+
if (entry.runId && entry.runId !== runId)
|
|
563
|
+
return false;
|
|
564
|
+
return entry.stage === stage && entry.agent === agent;
|
|
565
|
+
});
|
|
566
|
+
for (const entry of computeActiveSubagents(sameRun)) {
|
|
567
|
+
return entry;
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
378
571
|
async function writeSubagentTracker(projectRoot, entries) {
|
|
379
|
-
const active = entries
|
|
380
|
-
.filter((entry) => entry.status === "scheduled" || entry.status === "launched" || entry.status === "acknowledged")
|
|
381
|
-
.map((entry) => ({
|
|
572
|
+
const active = computeActiveSubagents(entries).map((entry) => ({
|
|
382
573
|
spanId: entry.spanId,
|
|
383
574
|
dispatchId: entry.dispatchId,
|
|
384
575
|
workerRunId: entry.workerRunId,
|
|
@@ -389,7 +580,8 @@ async function writeSubagentTracker(projectRoot, entries) {
|
|
|
389
580
|
agentDefinitionPath: entry.agentDefinitionPath,
|
|
390
581
|
startedAt: entry.startTs,
|
|
391
582
|
launchedAt: entry.launchedTs,
|
|
392
|
-
acknowledgedAt: entry.ackTs
|
|
583
|
+
acknowledgedAt: entry.ackTs,
|
|
584
|
+
allowParallel: entry.allowParallel
|
|
393
585
|
}));
|
|
394
586
|
await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
395
587
|
}
|
|
@@ -398,7 +590,20 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
398
590
|
await withDirectoryLock(delegationLockPath(projectRoot), async () => {
|
|
399
591
|
const filePath = delegationLogPath(projectRoot);
|
|
400
592
|
const prior = await readDelegationLedger(projectRoot);
|
|
401
|
-
|
|
593
|
+
// Span start anchor: prefer explicit `startTs`; otherwise fall back to
|
|
594
|
+
// the earliest provided lifecycle marker so the monotonic validator
|
|
595
|
+
// never sees a synthetic `now` overshoot a real event timestamp.
|
|
596
|
+
const lifecycleCandidates = [
|
|
597
|
+
entry.startTs,
|
|
598
|
+
entry.launchedTs,
|
|
599
|
+
entry.ackTs,
|
|
600
|
+
entry.completedTs,
|
|
601
|
+
entry.ts
|
|
602
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
603
|
+
const earliestLifecycle = lifecycleCandidates.length > 0
|
|
604
|
+
? lifecycleCandidates.reduce((min, candidate) => (candidate < min ? candidate : min))
|
|
605
|
+
: undefined;
|
|
606
|
+
const startTs = entry.startTs ?? earliestLifecycle ?? new Date().toISOString();
|
|
402
607
|
if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
|
|
403
608
|
throw new Error("waived delegation entries require a non-empty waiverReason");
|
|
404
609
|
}
|
|
@@ -442,6 +647,18 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
442
647
|
if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
|
|
443
648
|
return;
|
|
444
649
|
}
|
|
650
|
+
validateMonotonicTimestamps(stamped, prior.entries);
|
|
651
|
+
if (stamped.status === "scheduled" && stamped.allowParallel !== true) {
|
|
652
|
+
const existing = findActiveSpanForPair(stamped.stage, stamped.agent, activeRunId, prior);
|
|
653
|
+
if (existing && existing.spanId && existing.spanId !== stamped.spanId) {
|
|
654
|
+
throw new DispatchDuplicateError({
|
|
655
|
+
existingSpanId: existing.spanId,
|
|
656
|
+
existingStatus: existing.status,
|
|
657
|
+
newSpanId: stamped.spanId,
|
|
658
|
+
pair: { stage: stamped.stage, agent: stamped.agent }
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
445
662
|
await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
|
|
446
663
|
const ledger = {
|
|
447
664
|
runId: activeRunId,
|
|
@@ -12,6 +12,7 @@ import { extractReviewLoopEnvelopeFromArtifact } from "../../content/review-loop
|
|
|
12
12
|
import { unique } from "./helpers.js";
|
|
13
13
|
import { AUTO_REVIEW_LOOP_GATE_BY_STAGE, reviewLoopArtifactFixHint, reviewLoopEnvelopeExample, validateGateEvidenceShape } from "./review-loop.js";
|
|
14
14
|
import { ensureProactiveDelegationTrace } from "./proactive-delegation-trace.js";
|
|
15
|
+
import { consumeWaiverToken } from "../waiver-grant.js";
|
|
15
16
|
function resolveSuccessorTransition(stage, track, transitionTargets, satisfiedGuards, selectedTransitionGuards) {
|
|
16
17
|
const natural = transitionTargets[0] ?? null;
|
|
17
18
|
const specialTargets = transitionTargets.filter((target) => target !== natural);
|
|
@@ -542,9 +543,30 @@ export async function runAdvanceStage(projectRoot, args, io) {
|
|
|
542
543
|
}
|
|
543
544
|
return 1;
|
|
544
545
|
}
|
|
546
|
+
let approvalRecord = null;
|
|
547
|
+
if (args.acceptProactiveWaiver) {
|
|
548
|
+
const tokenRaw = args.acceptProactiveWaiverToken?.trim() ?? "";
|
|
549
|
+
if (tokenRaw.length === 0) {
|
|
550
|
+
io.stderr.write(`cclaw internal advance-stage: --accept-proactive-waiver now requires =<token>. Run \`cclaw-cli internal waiver-grant --stage ${args.stage} --reason "<why safe>"\` to issue one, then rerun with --accept-proactive-waiver=<token>.\n`);
|
|
551
|
+
return 2;
|
|
552
|
+
}
|
|
553
|
+
const consumed = await consumeWaiverToken(projectRoot, {
|
|
554
|
+
stage: args.stage,
|
|
555
|
+
token: tokenRaw,
|
|
556
|
+
consumedBy: "advance-stage"
|
|
557
|
+
});
|
|
558
|
+
if (!consumed.ok) {
|
|
559
|
+
io.stderr.write(`cclaw internal advance-stage: waiver token rejected (${consumed.reason}): ${consumed.detail}. Issue a fresh token via \`cclaw-cli internal waiver-grant --stage ${args.stage} --reason "<why safe>"\`.\n`);
|
|
560
|
+
return 2;
|
|
561
|
+
}
|
|
562
|
+
approvalRecord = consumed.record;
|
|
563
|
+
}
|
|
545
564
|
const proactiveTrace = await ensureProactiveDelegationTrace(projectRoot, args.stage, {
|
|
546
565
|
acceptWaiver: args.acceptProactiveWaiver,
|
|
547
566
|
waiverReason: args.acceptProactiveWaiverReason,
|
|
567
|
+
approvalToken: approvalRecord?.token,
|
|
568
|
+
approvalReason: approvalRecord?.reason,
|
|
569
|
+
approvalIssuedAt: approvalRecord?.issuedAt,
|
|
548
570
|
discoveryMode: flowState.discoveryMode,
|
|
549
571
|
repoSignals: flowState.repoSignals
|
|
550
572
|
});
|
|
@@ -600,7 +622,7 @@ export async function runAdvanceStage(projectRoot, args, io) {
|
|
|
600
622
|
currentStage: successor ?? args.stage,
|
|
601
623
|
interactionHints
|
|
602
624
|
};
|
|
603
|
-
await writeFlowState(projectRoot, finalState);
|
|
625
|
+
await writeFlowState(projectRoot, finalState, { writerSubsystem: "advance-stage" });
|
|
604
626
|
if (args.quiet) {
|
|
605
627
|
io.stdout.write(`${JSON.stringify({
|
|
606
628
|
ok: true,
|
|
@@ -8,6 +8,14 @@ export interface AdvanceStageArgs {
|
|
|
8
8
|
waiverReason?: string;
|
|
9
9
|
acceptProactiveWaiver: boolean;
|
|
10
10
|
acceptProactiveWaiverReason?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Approval token issued by `cclaw-cli internal waiver-grant`. Required
|
|
13
|
+
* (via `--accept-proactive-waiver=<token>`) whenever the caller asserts
|
|
14
|
+
* `acceptProactiveWaiver`. Legacy `--accept-proactive-waiver` without a
|
|
15
|
+
* token is still parsed but rejected downstream by the advance-stage
|
|
16
|
+
* handler so operators see the error at runtime.
|
|
17
|
+
*/
|
|
18
|
+
acceptProactiveWaiverToken?: string;
|
|
11
19
|
skipQuestions: boolean;
|
|
12
20
|
quiet: boolean;
|
|
13
21
|
json: boolean;
|
|
@@ -12,6 +12,7 @@ export function parseAdvanceStageArgs(tokens) {
|
|
|
12
12
|
let waiverReason;
|
|
13
13
|
let acceptProactiveWaiver = false;
|
|
14
14
|
let acceptProactiveWaiverReason;
|
|
15
|
+
let acceptProactiveWaiverToken;
|
|
15
16
|
let skipQuestions = false;
|
|
16
17
|
let quiet = false;
|
|
17
18
|
let json = false;
|
|
@@ -81,6 +82,11 @@ export function parseAdvanceStageArgs(tokens) {
|
|
|
81
82
|
acceptProactiveWaiver = true;
|
|
82
83
|
continue;
|
|
83
84
|
}
|
|
85
|
+
if (token.startsWith("--accept-proactive-waiver=")) {
|
|
86
|
+
acceptProactiveWaiver = true;
|
|
87
|
+
acceptProactiveWaiverToken = token.slice("--accept-proactive-waiver=".length).trim();
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
84
90
|
if (token === "--skip-questions") {
|
|
85
91
|
skipQuestions = true;
|
|
86
92
|
continue;
|
|
@@ -107,6 +113,7 @@ export function parseAdvanceStageArgs(tokens) {
|
|
|
107
113
|
waiverReason,
|
|
108
114
|
acceptProactiveWaiver,
|
|
109
115
|
acceptProactiveWaiverReason,
|
|
116
|
+
acceptProactiveWaiverToken,
|
|
110
117
|
skipQuestions,
|
|
111
118
|
quiet,
|
|
112
119
|
json
|
|
@@ -16,6 +16,9 @@ export interface ProactiveDelegationTraceResult {
|
|
|
16
16
|
export declare function ensureProactiveDelegationTrace(projectRoot: string, stage: FlowStage, options: {
|
|
17
17
|
acceptWaiver: boolean;
|
|
18
18
|
waiverReason?: string;
|
|
19
|
+
approvalToken?: string;
|
|
20
|
+
approvalReason?: string;
|
|
21
|
+
approvalIssuedAt?: string;
|
|
19
22
|
discoveryMode: DiscoveryMode;
|
|
20
23
|
repoSignals?: RepoSignals;
|
|
21
24
|
}): Promise<ProactiveDelegationTraceResult>;
|
|
@@ -31,7 +31,11 @@ export async function ensureProactiveDelegationTrace(projectRoot, stage, options
|
|
|
31
31
|
return { missingRules: [] };
|
|
32
32
|
if (!options.acceptWaiver)
|
|
33
33
|
return { missingRules };
|
|
34
|
-
const
|
|
34
|
+
const approvalToken = options.approvalToken?.trim();
|
|
35
|
+
const approvalReason = options.approvalReason?.trim();
|
|
36
|
+
const waiverReason = options.waiverReason?.trim() ||
|
|
37
|
+
approvalReason ||
|
|
38
|
+
"accepted via --accept-proactive-waiver";
|
|
35
39
|
for (const rule of missingRules) {
|
|
36
40
|
await appendDelegation(projectRoot, {
|
|
37
41
|
stage,
|
|
@@ -42,6 +46,9 @@ export async function ensureProactiveDelegationTrace(projectRoot, stage, options
|
|
|
42
46
|
acceptedBy: "user-flag",
|
|
43
47
|
conditionTrigger: rule.when,
|
|
44
48
|
skill: rule.skill,
|
|
49
|
+
...(approvalToken ? { approvalToken } : {}),
|
|
50
|
+
...(approvalReason ? { approvalReason } : {}),
|
|
51
|
+
...(options.approvalIssuedAt ? { approvalIssuedAt: options.approvalIssuedAt } : {}),
|
|
45
52
|
ts: new Date().toISOString()
|
|
46
53
|
});
|
|
47
54
|
}
|
|
@@ -40,7 +40,7 @@ export async function runRewind(projectRoot, args, io) {
|
|
|
40
40
|
const staleStages = { ...current.staleStages };
|
|
41
41
|
delete staleStages[args.targetStage];
|
|
42
42
|
const nextState = { ...current, staleStages };
|
|
43
|
-
await writeFlowState(projectRoot, nextState);
|
|
43
|
+
await writeFlowState(projectRoot, nextState, { writerSubsystem: "rewind-ack" });
|
|
44
44
|
const payload = {
|
|
45
45
|
ok: true,
|
|
46
46
|
command: "rewind",
|
|
@@ -85,7 +85,7 @@ export async function runRewind(projectRoot, args, io) {
|
|
|
85
85
|
staleStages,
|
|
86
86
|
rewinds: [...current.rewinds, record]
|
|
87
87
|
};
|
|
88
|
-
await writeFlowState(projectRoot, nextState);
|
|
88
|
+
await writeFlowState(projectRoot, nextState, { writerSubsystem: "rewind" });
|
|
89
89
|
const payload = {
|
|
90
90
|
ok: true,
|
|
91
91
|
command: "rewind",
|
|
@@ -209,7 +209,10 @@ export async function runStartFlow(projectRoot, args, io) {
|
|
|
209
209
|
}
|
|
210
210
|
const repoSignals = await collectRepoSignals(projectRoot);
|
|
211
211
|
nextState = { ...nextState, repoSignals };
|
|
212
|
-
await writeFlowState(projectRoot, nextState, {
|
|
212
|
+
await writeFlowState(projectRoot, nextState, {
|
|
213
|
+
allowReset: true,
|
|
214
|
+
writerSubsystem: "start-flow"
|
|
215
|
+
});
|
|
213
216
|
await appendIdeaArtifact(projectRoot, args, current);
|
|
214
217
|
const successPayload = {
|
|
215
218
|
ok: true,
|
|
@@ -11,13 +11,34 @@ import { runRewind } from "./advance-stage/rewind.js";
|
|
|
11
11
|
import { runVerifyFlowStateDiff, runVerifyCurrentState } from "./advance-stage/verify.js";
|
|
12
12
|
import { runHookCommand } from "./advance-stage/hook.js";
|
|
13
13
|
import { parseAdvanceStageArgs, parseCancelRunArgs, parseHookArgs, parseRewindArgs, parseStartFlowArgs, parseVerifyCurrentStateArgs, parseVerifyFlowStateDiffArgs } from "./advance-stage/parsers.js";
|
|
14
|
+
import { parseFlowStateRepairArgs, runFlowStateRepair } from "./flow-state-repair.js";
|
|
15
|
+
import { parseWaiverGrantArgs, runWaiverGrant } from "./waiver-grant.js";
|
|
16
|
+
import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persistence.js";
|
|
17
|
+
import { DelegationTimestampError, DispatchDuplicateError } from "../delegation.js";
|
|
18
|
+
/**
|
|
19
|
+
* Subcommands that mutate or consult flow-state.json via the CLI runtime.
|
|
20
|
+
* They all require the sha256 sidecar to match before continuing so a
|
|
21
|
+
* manual edit hard-blocks with exit code 2 (same contract as the inline
|
|
22
|
+
* hook checks).
|
|
23
|
+
*/
|
|
24
|
+
const GUARD_ENFORCED_SUBCOMMANDS = new Set([
|
|
25
|
+
"advance-stage",
|
|
26
|
+
"start-flow",
|
|
27
|
+
"cancel-run",
|
|
28
|
+
"rewind",
|
|
29
|
+
"verify-flow-state-diff",
|
|
30
|
+
"verify-current-state"
|
|
31
|
+
]);
|
|
14
32
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
15
33
|
const [subcommand, ...tokens] = argv;
|
|
16
34
|
if (!subcommand) {
|
|
17
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook\n");
|
|
35
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n");
|
|
18
36
|
return 1;
|
|
19
37
|
}
|
|
20
38
|
try {
|
|
39
|
+
if (GUARD_ENFORCED_SUBCOMMANDS.has(subcommand)) {
|
|
40
|
+
await verifyFlowStateGuard(projectRoot);
|
|
41
|
+
}
|
|
21
42
|
if (subcommand === "advance-stage") {
|
|
22
43
|
return await runAdvanceStage(projectRoot, parseAdvanceStageArgs(tokens), io);
|
|
23
44
|
}
|
|
@@ -57,10 +78,28 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
57
78
|
if (subcommand === "hook") {
|
|
58
79
|
return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
|
|
59
80
|
}
|
|
60
|
-
|
|
81
|
+
if (subcommand === "flow-state-repair") {
|
|
82
|
+
return await runFlowStateRepair(projectRoot, parseFlowStateRepairArgs(tokens), io);
|
|
83
|
+
}
|
|
84
|
+
if (subcommand === "waiver-grant") {
|
|
85
|
+
return await runWaiverGrant(projectRoot, parseWaiverGrantArgs(tokens), io);
|
|
86
|
+
}
|
|
87
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n`);
|
|
61
88
|
return 1;
|
|
62
89
|
}
|
|
63
90
|
catch (err) {
|
|
91
|
+
if (err instanceof FlowStateGuardMismatchError) {
|
|
92
|
+
io.stderr.write(`cclaw internal ${subcommand}: ${err.message}\n`);
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
if (err instanceof DelegationTimestampError) {
|
|
96
|
+
io.stderr.write(`error: delegation_timestamp_non_monotonic — ${err.field}: ${err.actual} < ${err.priorBound}\n`);
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
if (err instanceof DispatchDuplicateError) {
|
|
100
|
+
io.stderr.write(`error: dispatch_duplicate — ${err.message}\n`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
64
103
|
io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
65
104
|
return 1;
|
|
66
105
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
interface InternalIo {
|
|
3
|
+
stdout: Writable;
|
|
4
|
+
stderr: Writable;
|
|
5
|
+
}
|
|
6
|
+
export interface FlowStateRepairArgs {
|
|
7
|
+
reason: string;
|
|
8
|
+
json: boolean;
|
|
9
|
+
quiet: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseFlowStateRepairArgs(tokens: string[]): FlowStateRepairArgs;
|
|
12
|
+
export declare function runFlowStateRepair(projectRoot: string, args: FlowStateRepairArgs, io: InternalIo): Promise<number>;
|
|
13
|
+
export {};
|