@ngockhoale/ukit 2.5.1 → 2.6.2
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/CHANGELOG.md +80 -0
- package/manifests/platform.full.yaml +16 -0
- package/package.json +1 -1
- package/src/cli/commands/install.js +49 -2
- package/src/cli/commands/update.js +5 -0
- package/src/core/output/index.js +77 -0
- package/src/core/update.js +36 -5
- package/templates/.claude/agents/code-reviewer.md +12 -1
- package/templates/.claude/agents/feature-implementer.md +4 -2
- package/templates/.claude/agents/handoff-planner.md +36 -1
- package/templates/.claude/commands/ukit/handoff-clear.md +4 -0
- package/templates/.claude/commands/ukit/handoff-create.md +23 -7
- package/templates/.claude/commands/ukit/handoff-fullstack.md +181 -17
- package/templates/.claude/commands/ukit/handoff-implement.md +9 -2
- package/templates/.claude/commands/ukit/handoff-review.md +4 -1
- package/templates/.claude/commands/ukit/handoff-status.md +6 -2
- package/templates/.claude/hooks/auto-allow-bash.sh +17 -5
- package/templates/.claude/hooks/block-dangerous.sh +18 -5
- package/templates/.claude/hooks/completion-gate.sh +17 -5
- package/templates/.claude/hooks/compress-output.sh +15 -6
- package/templates/.claude/hooks/context-hardcap-gate.sh +77 -46
- package/templates/.claude/hooks/context-window-guard.sh +41 -27
- package/templates/.claude/hooks/handoff-model-guard.sh +62 -14
- package/templates/.claude/hooks/handoff-resume.sh +20 -7
- package/templates/.claude/hooks/post-edit-verify.sh +17 -5
- package/templates/.claude/hooks/pre-edit-backup.sh +17 -5
- package/templates/.claude/hooks/project-important.sh +18 -1
- package/templates/.claude/hooks/protect-files.sh +18 -5
- package/templates/.claude/hooks/record-execution.sh +17 -5
- package/templates/.claude/hooks/sensitive-data-guard.sh +48 -9
- package/templates/.claude/hooks/skill-router.sh +124 -86
- package/templates/.claude/hooks/stale-spec-guard.sh +22 -6
- package/templates/.claude/hooks/task-watchdog.sh +22 -7
- package/templates/.claude/hooks/verification-guard.sh +17 -5
- package/templates/.claude/hooks/vision-router.sh +17 -5
- package/templates/.claude/settings.json +15 -10
- package/templates/.claude/ukit/index/provision-worktree.mjs +30 -2
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +99 -1
- package/templates/.claude/ukit/runtime/hook-input.sh +25 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +86 -2
- package/templates/.claude/ukit/runtime/output-compression.mjs +87 -0
- package/templates/.claude/ukit/runtime/stop-coordinator.mjs +201 -11
- package/templates/.omp/RULES.md +9 -1
- package/templates/.omp/agents/code-reviewer.md +12 -1
- package/templates/.omp/agents/feature-implementer.md +4 -2
- package/templates/.omp/agents/handoff-planner.md +36 -1
- package/templates/.omp/hooks/pre/ukit-bridge.js +110 -3
- package/templates/AGENTS.md +14 -0
- package/templates/CLAUDE.md +14 -0
- package/templates/docs/AI_HANDOFF/RULES.md +37 -4
- package/templates/docs/AI_HANDOFF/SPEC.md +98 -0
- package/templates/docs/AI_HANDOFF/tasks/_TEMPLATE.md +4 -1
- package/templates/ukit/storage/config.json +48 -9
|
@@ -12,9 +12,18 @@
|
|
|
12
12
|
* `--evaluate-stop` exactly once (the ledger owns its own counting semantics);
|
|
13
13
|
* - it evaluates the watchdog policy in-process through task-watchdog.mjs's
|
|
14
14
|
* exported `evaluateStopWatchdog` exactly once;
|
|
15
|
-
* - it
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* - it evaluates the handoff-cursor policy in-process: while
|
|
16
|
+
* `docs/AI_HANDOFF/RUN.md` carries `Phase:` other than `done`/`blocked`, a
|
|
17
|
+
* handoff-fullstack run is still in flight and the stop is bounced back with
|
|
18
|
+
* the cursor's `Next:` step — this is what keeps an overnight run moving
|
|
19
|
+
* after a recap instead of stalling idle. A stalled-cursor breaker releases
|
|
20
|
+
* the stop if the cursor has not advanced across `stopGateMaxStalledBlocks`
|
|
21
|
+
* consecutive blocked stops (same liveness shape as the ledger's
|
|
22
|
+
* noProgressCount breaker);
|
|
23
|
+
* - it merges the three evaluator results with a deterministic owner order —
|
|
24
|
+
* fail-closed completion failure > completion block > handoff-cursor block >
|
|
25
|
+
* watchdog block > merged advisory > silent release — and emits AT MOST ONE
|
|
26
|
+
* Stop JSON line;
|
|
18
27
|
* - a lock-scoped once-per-stop guard makes a legacy double-registered Stop
|
|
19
28
|
* (old settings.json still listing both hooks) inert: the second coordinator
|
|
20
29
|
* invocation for the same session inside the dedupe window emits nothing and
|
|
@@ -41,6 +50,7 @@
|
|
|
41
50
|
* normalizeEvaluatorStdout — ledger stdout → { kind, reason?, systemMessage? }.
|
|
42
51
|
* coordinateStopDecisions — the pure ordered merge; owns ordering + provenance
|
|
43
52
|
* only (evaluators stay independently testable).
|
|
53
|
+
* evaluateHandoffCursor — in-process RUN.md lane; advisory on any failure.
|
|
44
54
|
* evaluateCompletionPolicy — spawns the ledger once, returns the normalized
|
|
45
55
|
* completion evaluator result (throws on failure).
|
|
46
56
|
* runStopCoordinator — event guard + dedupe + both evaluators + merge.
|
|
@@ -162,6 +172,20 @@ const LOCK_BUDGET_MS = 600;
|
|
|
162
172
|
|
|
163
173
|
const WATCHDOG_PROVENANCE_PREFIX = '[ukit-stop-coordinator] task-watchdog also requested a block: ';
|
|
164
174
|
const WATCHDOG_FAILURE_PREFIX = '[ukit-stop-coordinator] task-watchdog evaluator failed (advisory lane, reported verbatim): ';
|
|
175
|
+
const HANDOFF_PROVENANCE_PREFIX = '[ukit-stop-coordinator] handoff-cursor also requested a block: ';
|
|
176
|
+
const HANDOFF_FAILURE_PREFIX = '[ukit-stop-coordinator] handoff-cursor evaluator failed (advisory lane, reported verbatim): ';
|
|
177
|
+
|
|
178
|
+
// Handoff-cursor lane defaults. Both are overridable through
|
|
179
|
+
// `.ukit/storage/config.json` → `handoff.fullstack.*`; any read/parse error fails
|
|
180
|
+
// OPEN to these values — the lane is advisory and must never wedge a session.
|
|
181
|
+
export const HANDOFF_CURSOR_DEFAULTS = Object.freeze({
|
|
182
|
+
enabled: true,
|
|
183
|
+
// A cursor that has not advanced across this many consecutive blocked stops is
|
|
184
|
+
// a run that can no longer make progress (wedged wave, dead dependency). The
|
|
185
|
+
// gate releases instead of trapping the session in an unstoppable loop — same
|
|
186
|
+
// liveness shape as the ledger's noProgressCount breaker.
|
|
187
|
+
maxStalledBlocks: 12,
|
|
188
|
+
});
|
|
165
189
|
|
|
166
190
|
// ─── Redaction ────────────────────────────────────────────────────────────
|
|
167
191
|
/**
|
|
@@ -223,24 +247,33 @@ export function normalizeEvaluatorStdout(stdout) {
|
|
|
223
247
|
* advisory-lane failures travel in the single systemMessage) — one decision, no
|
|
224
248
|
* duplicate prompts.
|
|
225
249
|
*/
|
|
226
|
-
export function coordinateStopDecisions({ completion = null, watchdog = null, failures = {} } = {}) {
|
|
250
|
+
export function coordinateStopDecisions({ completion = null, watchdog = null, handoff = null, failures = {} } = {}) {
|
|
227
251
|
const completionFailed = typeof failures?.completion === 'string' && failures.completion.length > 0;
|
|
228
252
|
const watchdogFailed = typeof failures?.watchdog === 'string' && failures.watchdog.length > 0;
|
|
253
|
+
const handoffFailed = typeof failures?.handoff === 'string' && failures.handoff.length > 0;
|
|
229
254
|
const watchdogFailureNote = watchdogFailed
|
|
230
255
|
? `${WATCHDOG_FAILURE_PREFIX}${failures.watchdog}`
|
|
231
256
|
: null;
|
|
257
|
+
const handoffFailureNote = handoffFailed
|
|
258
|
+
? `${HANDOFF_FAILURE_PREFIX}${failures.handoff}`
|
|
259
|
+
: null;
|
|
232
260
|
const watchdogAdvisory = watchdog?.kind === 'advisory' ? watchdog.systemMessage : null;
|
|
261
|
+
const handoffAdvisory = handoff?.kind === 'advisory' ? handoff.systemMessage : null;
|
|
233
262
|
const policies = [];
|
|
234
263
|
|
|
235
264
|
// 1. The fail-closed blocker itself failed → block, redacted, detail on stderr only.
|
|
236
265
|
if (completionFailed) {
|
|
237
266
|
policies.push('completion-gate');
|
|
238
267
|
let reason = redactInfrastructureReason();
|
|
268
|
+
if (handoff?.kind === 'block') {
|
|
269
|
+
policies.push('handoff-cursor');
|
|
270
|
+
reason += `\n${HANDOFF_PROVENANCE_PREFIX}${handoff.reason}`;
|
|
271
|
+
}
|
|
239
272
|
if (watchdog?.kind === 'block') {
|
|
240
273
|
policies.push('task-watchdog');
|
|
241
274
|
reason += `\n${WATCHDOG_PROVENANCE_PREFIX}${watchdog.reason}`;
|
|
242
275
|
}
|
|
243
|
-
const notes = [watchdogFailureNote, watchdogAdvisory].filter(Boolean);
|
|
276
|
+
const notes = [watchdogFailureNote, handoffFailureNote, watchdogAdvisory, handoffAdvisory].filter(Boolean);
|
|
244
277
|
return {
|
|
245
278
|
decision: 'block',
|
|
246
279
|
reason,
|
|
@@ -254,11 +287,15 @@ export function coordinateStopDecisions({ completion = null, watchdog = null, fa
|
|
|
254
287
|
if (completion?.kind === 'block') {
|
|
255
288
|
policies.push('completion-gate');
|
|
256
289
|
let reason = completion.reason;
|
|
290
|
+
if (handoff?.kind === 'block') {
|
|
291
|
+
policies.push('handoff-cursor');
|
|
292
|
+
reason += `\n${HANDOFF_PROVENANCE_PREFIX}${handoff.reason}`;
|
|
293
|
+
}
|
|
257
294
|
if (watchdog?.kind === 'block') {
|
|
258
295
|
policies.push('task-watchdog');
|
|
259
296
|
reason += `\n${WATCHDOG_PROVENANCE_PREFIX}${watchdog.reason}`;
|
|
260
297
|
}
|
|
261
|
-
const notes = [watchdogFailureNote, watchdogAdvisory].filter(Boolean);
|
|
298
|
+
const notes = [watchdogFailureNote, handoffFailureNote, watchdogAdvisory, handoffAdvisory].filter(Boolean);
|
|
262
299
|
return {
|
|
263
300
|
decision: 'block',
|
|
264
301
|
reason,
|
|
@@ -268,11 +305,30 @@ export function coordinateStopDecisions({ completion = null, watchdog = null, fa
|
|
|
268
305
|
};
|
|
269
306
|
}
|
|
270
307
|
|
|
271
|
-
// 3.
|
|
308
|
+
// 3. Handoff-cursor block → an unfinished handoff-fullstack run owns this stop.
|
|
309
|
+
if (handoff?.kind === 'block') {
|
|
310
|
+
policies.push('handoff-cursor');
|
|
311
|
+
let reason = handoff.reason;
|
|
312
|
+
if (watchdog?.kind === 'block') {
|
|
313
|
+
policies.push('task-watchdog');
|
|
314
|
+
reason += `\n${WATCHDOG_PROVENANCE_PREFIX}${watchdog.reason}`;
|
|
315
|
+
}
|
|
316
|
+
const notes = [completion?.systemMessage, watchdogFailureNote, watchdogAdvisory].filter(Boolean);
|
|
317
|
+
return {
|
|
318
|
+
decision: 'block',
|
|
319
|
+
reason,
|
|
320
|
+
systemMessage: notes.length > 0 ? notes.join('\n') : undefined,
|
|
321
|
+
policyOwner: 'handoff-cursor',
|
|
322
|
+
policies,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 4. Watchdog block → watchdog owns this stop; other advisories ride along.
|
|
272
327
|
if (watchdog?.kind === 'block') {
|
|
273
328
|
policies.push('task-watchdog');
|
|
274
329
|
if (completion?.kind === 'advisory') policies.push('completion-gate');
|
|
275
|
-
|
|
330
|
+
if (handoff?.kind === 'advisory') policies.push('handoff-cursor');
|
|
331
|
+
const notes = [completion?.systemMessage, handoffAdvisory, watchdogFailureNote, handoffFailureNote].filter(Boolean);
|
|
276
332
|
return {
|
|
277
333
|
decision: 'block',
|
|
278
334
|
reason: watchdog.reason,
|
|
@@ -282,10 +338,11 @@ export function coordinateStopDecisions({ completion = null, watchdog = null, fa
|
|
|
282
338
|
};
|
|
283
339
|
}
|
|
284
340
|
|
|
285
|
-
//
|
|
341
|
+
// 5. No block → merge advisories + verbatim failures into ONE systemMessage, or stay silent.
|
|
286
342
|
if (completion?.kind === 'advisory') policies.push('completion-gate');
|
|
287
343
|
if (watchdog?.kind === 'advisory') policies.push('task-watchdog');
|
|
288
|
-
|
|
344
|
+
if (handoff?.kind === 'advisory') policies.push('handoff-cursor');
|
|
345
|
+
const notes = [completion?.systemMessage, watchdogAdvisory, handoffAdvisory, watchdogFailureNote, handoffFailureNote].filter(Boolean);
|
|
289
346
|
return {
|
|
290
347
|
decision: null,
|
|
291
348
|
reason: undefined,
|
|
@@ -359,6 +416,131 @@ export async function evaluateCompletionPolicy({ projectRoot, rawInput, budgetMs
|
|
|
359
416
|
return normalizeEvaluatorStdout(stdout);
|
|
360
417
|
}
|
|
361
418
|
|
|
419
|
+
// ─── Handoff-cursor evaluator (in-process, advisory-on-failure) ───────────
|
|
420
|
+
/**
|
|
421
|
+
* Read `.ukit/storage/config.json` → `handoff.fullstack.stopGate*` merged over
|
|
422
|
+
* HANDOFF_CURSOR_DEFAULTS. Any failure returns the defaults — a broken config
|
|
423
|
+
* must never silence the lane outright NOR wedge it; defaults keep both sane.
|
|
424
|
+
*/
|
|
425
|
+
async function loadHandoffGateConfig(projectRoot) {
|
|
426
|
+
try {
|
|
427
|
+
const parsed = JSON.parse(
|
|
428
|
+
await fs.readFile(path.join(projectRoot, '.ukit', 'storage', 'config.json'), 'utf8'),
|
|
429
|
+
);
|
|
430
|
+
const gate = parsed?.handoff?.fullstack;
|
|
431
|
+
return {
|
|
432
|
+
enabled: typeof gate?.stopGateEnabled === 'boolean' ? gate.stopGateEnabled : HANDOFF_CURSOR_DEFAULTS.enabled,
|
|
433
|
+
maxStalledBlocks: Number.isFinite(gate?.stopGateMaxStalledBlocks)
|
|
434
|
+
? Math.max(1, gate.stopGateMaxStalledBlocks)
|
|
435
|
+
: HANDOFF_CURSOR_DEFAULTS.maxStalledBlocks,
|
|
436
|
+
};
|
|
437
|
+
} catch {
|
|
438
|
+
return { ...HANDOFF_CURSOR_DEFAULTS };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function parseRunCursor(text) {
|
|
443
|
+
const field = (name) => (String(text).match(new RegExp(`^${name}:\\s*(.+)$`, 'm'))?.[1] || '').trim();
|
|
444
|
+
return {
|
|
445
|
+
command: field('Command'),
|
|
446
|
+
goal: field('Goal'),
|
|
447
|
+
phase: field('Phase'),
|
|
448
|
+
cursor: field('Cursor'),
|
|
449
|
+
next: field('Next'),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Mutate `.ukit/storage/cache/stop-coordinator/state.json`'s `handoff` slot:
|
|
455
|
+
* { signature, count }. Same signature as last time → count+1; a moved cursor
|
|
456
|
+
* resets the streak. Returns the committed streak count; a lock-timeout or any
|
|
457
|
+
* I/O failure returns null (fail-open — never decides the stop).
|
|
458
|
+
*/
|
|
459
|
+
async function bumpHandoffStallCount({ projectRoot, signature, lockBudgetMs }) {
|
|
460
|
+
const statePath = path.join(projectRoot, '.ukit', 'storage', 'cache', 'stop-coordinator', 'state.json');
|
|
461
|
+
try {
|
|
462
|
+
const outcome = await withAsyncLock(statePath, { deadlineMs: lockBudgetMs }, async () => {
|
|
463
|
+
let current = {};
|
|
464
|
+
try {
|
|
465
|
+
current = JSON.parse(await fs.readFile(statePath, 'utf8')) || {};
|
|
466
|
+
} catch {}
|
|
467
|
+
const prev = current.handoff || {};
|
|
468
|
+
const count = prev.signature === signature ? (Number(prev.count) || 0) + 1 : 1;
|
|
469
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
470
|
+
await fs.writeFile(
|
|
471
|
+
statePath,
|
|
472
|
+
`${JSON.stringify({ ...current, handoff: { signature, count } }, null, 1)}\n`,
|
|
473
|
+
'utf8',
|
|
474
|
+
);
|
|
475
|
+
return count;
|
|
476
|
+
});
|
|
477
|
+
return outcome.ok ? outcome.value : null;
|
|
478
|
+
} catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The handoff-cursor policy lane.
|
|
485
|
+
*
|
|
486
|
+
* BLOCKS the stop while `docs/AI_HANDOFF/RUN.md` reports an in-flight phase —
|
|
487
|
+
* the run's own `Next:` line is the continuation instruction, so a recap or a
|
|
488
|
+
* premature "done" reply gets bounced straight back into the pipeline. Releases
|
|
489
|
+
* when the cursor says `done` (or `blocked` — a genuinely stuck run is a
|
|
490
|
+
* legitimate final report), when RUN.md is absent/unreadable, or when the same
|
|
491
|
+
* un-advancing cursor has already bounced `maxStalledBlocks` stops in a row
|
|
492
|
+
* (liveness breaker — the gate must never be an unstoppable loop).
|
|
493
|
+
*
|
|
494
|
+
* Advisory lane: EVERY failure path returns { kind: 'none' }, never throws.
|
|
495
|
+
*/
|
|
496
|
+
export async function evaluateHandoffCursor({ projectRoot, now = Date.now(), lockBudgetMs = LOCK_BUDGET_MS } = {}) {
|
|
497
|
+
const gate = await loadHandoffGateConfig(projectRoot);
|
|
498
|
+
if (!gate.enabled) return { kind: 'none' };
|
|
499
|
+
|
|
500
|
+
let text;
|
|
501
|
+
try {
|
|
502
|
+
text = await fs.readFile(path.join(projectRoot, 'docs', 'AI_HANDOFF', 'RUN.md'), 'utf8');
|
|
503
|
+
} catch {
|
|
504
|
+
return { kind: 'none' };
|
|
505
|
+
}
|
|
506
|
+
const { goal, phase, cursor, next } = parseRunCursor(text);
|
|
507
|
+
if (!phase || /^done\b/i.test(phase) || /^blocked\b/i.test(phase)) {
|
|
508
|
+
return { kind: 'none' };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const signature = `${phase}|${cursor}|${next}`;
|
|
512
|
+
const streak = await bumpHandoffStallCount({ projectRoot, signature, lockBudgetMs });
|
|
513
|
+
if (streak !== null && streak > gate.maxStalledBlocks) {
|
|
514
|
+
return {
|
|
515
|
+
kind: 'advisory',
|
|
516
|
+
systemMessage:
|
|
517
|
+
`[ukit-stop-coordinator] handoff-cursor released this stop: the run cursor ` +
|
|
518
|
+
`("${signature}") has not advanced across ${streak - 1} consecutive blocked stops ` +
|
|
519
|
+
`(cap ${gate.maxStalledBlocks}), so the gate treats the run as stalled rather than ` +
|
|
520
|
+
`looping forever. Inspect docs/AI_HANDOFF/RUN.md and docs/AI_HANDOFF/INDEX.md; ` +
|
|
521
|
+
`the run can be resumed with /ukit:handoff-fullstack or cleared with /ukit:handoff-clear.`,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const lines = [
|
|
526
|
+
'UKit handoff run in progress — this stop is refused. A handoff-fullstack cycle is still',
|
|
527
|
+
'in flight; a recap, wave summary, or "waiting" reply is NOT completion. The only valid',
|
|
528
|
+
'terminal outputs are "HANDOFF FULLSTACK COMPLETE" (all gates passed, cursor set to',
|
|
529
|
+
'`Phase: done`) or "HANDOFF FULLSTACK BLOCKED" (every remaining task externally blocked,',
|
|
530
|
+
'cursor set to `Phase: blocked`).',
|
|
531
|
+
'',
|
|
532
|
+
` Goal: ${goal || '(not recorded)'}`,
|
|
533
|
+
` Phase: ${phase}`,
|
|
534
|
+
` Cursor: ${cursor || '(not recorded)'}`,
|
|
535
|
+
` Next: ${next || '(not recorded)'}`,
|
|
536
|
+
'',
|
|
537
|
+
'CONTINUE IMMEDIATELY: read docs/AI_HANDOFF/RUN.md and docs/AI_HANDOFF/INDEX.md, execute',
|
|
538
|
+
'the `Next:` step exactly, rewrite the run cursor, and keep working until the completion',
|
|
539
|
+
'gate in .claude/commands/ukit/handoff-fullstack.md passes.',
|
|
540
|
+
];
|
|
541
|
+
return { kind: 'block', reason: lines.join('\n') };
|
|
542
|
+
}
|
|
543
|
+
|
|
362
544
|
// ─── Once-per-stop guard ──────────────────────────────────────────────────
|
|
363
545
|
/**
|
|
364
546
|
* Lock-scoped dedupe: persist `{ lastStop: { key, ts } }` and report whether this
|
|
@@ -434,7 +616,15 @@ export async function runStopCoordinator({
|
|
|
434
616
|
process.stderr.write(`[ukit-stop-coordinator] task-watchdog evaluator failed: ${failures.watchdog}\n`);
|
|
435
617
|
}
|
|
436
618
|
|
|
437
|
-
|
|
619
|
+
let handoff = null;
|
|
620
|
+
try {
|
|
621
|
+
handoff = await evaluateHandoffCursor({ projectRoot, now, lockBudgetMs });
|
|
622
|
+
} catch (error) {
|
|
623
|
+
failures.handoff = error?.message || String(error);
|
|
624
|
+
process.stderr.write(`[ukit-stop-coordinator] handoff-cursor evaluator failed: ${failures.handoff}\n`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { skip: null, merged: coordinateStopDecisions({ completion, watchdog, handoff, failures }) };
|
|
438
628
|
}
|
|
439
629
|
|
|
440
630
|
// ─── CLI ──────────────────────────────────────────────────────────────────
|
package/templates/.omp/RULES.md
CHANGED
|
@@ -57,7 +57,15 @@ Prefer unique current-file anchors over line numbers or stale pasted blocks. Nev
|
|
|
57
57
|
stale spec — re-read current source and confirm before applying. Preserve existing BOM and line
|
|
58
58
|
endings.
|
|
59
59
|
|
|
60
|
-
## 8.
|
|
60
|
+
## 8. Handoff runs never park mid-cycle
|
|
61
|
+
|
|
62
|
+
While `docs/AI_HANDOFF/RUN.md` `Phase:` is not `done`/`blocked`, a handoff-fullstack run is
|
|
63
|
+
still in flight: a recap or checkpoint is progress output, never a stopping point. Continue
|
|
64
|
+
the cursor's `Next:` step; only `HANDOFF FULLSTACK COMPLETE` or `HANDOFF FULLSTACK BLOCKED`
|
|
65
|
+
ends the run. `handoff-clear` must close the cursor (`Phase: done` or delete) or the gate
|
|
66
|
+
keeps the next session running too.
|
|
67
|
+
|
|
68
|
+
## 9. Never end a turn silently
|
|
61
69
|
|
|
62
70
|
Ending a turn with no output is a defect, not a pause. If you stop before the work is finished, the
|
|
63
71
|
last thing you emit is one short line naming what is unfinished and what you need. Silence is never
|
|
@@ -126,17 +126,28 @@ Same model is the most common silent failure. Do not skip this check.
|
|
|
126
126
|
### Inputs you expect
|
|
127
127
|
|
|
128
128
|
- Path to the spec/plan document (e.g. `docs/plans/*.md`). No diff, no task file, no executor report — review the document itself.
|
|
129
|
+
- When invoked from the handoff pipeline you get BOTH `docs/AI_HANDOFF/SPEC.md` and `docs/AI_HANDOFF/PLAN.md`. Review them as one unit: the spec is the contract, the plan is the decomposition. Verdicts still append to PLAN.md's `## Plan Review Log`.
|
|
129
130
|
|
|
130
131
|
### Review order
|
|
131
132
|
|
|
132
133
|
| Category | What to look for |
|
|
133
134
|
|---|---|
|
|
134
135
|
| Completeness | TODO/TBD/placeholders, incomplete sections |
|
|
135
|
-
| Consistency | internal contradictions, conflicting requirements |
|
|
136
|
+
| Consistency | internal contradictions, conflicting requirements; SPEC and PLAN contradicting each other |
|
|
136
137
|
| Clarity | requirements ambiguous enough to cause a wrong build |
|
|
137
138
|
| Scope | focused enough for one plan, not silently covering multiple subsystems |
|
|
138
139
|
| YAGNI | unrequested features, over-engineering |
|
|
139
140
|
|
|
141
|
+
**Handoff spec quality gate** (only when reviewing `docs/AI_HANDOFF/SPEC.md`):
|
|
142
|
+
|
|
143
|
+
1. Every functional requirement is testable — Given/When/Then or a command, never "should work".
|
|
144
|
+
2. Every applicable fullstack layer is covered or explicitly `N/A` with a reason.
|
|
145
|
+
3. Every FR traces forward to plan scope; nothing in the plan is unbacked by the spec.
|
|
146
|
+
4. Dependencies between parts are explicit.
|
|
147
|
+
5. Legacy/unfinished work discovered by the Phase 0 sweep is either planned or recorded out-of-scope.
|
|
148
|
+
6. Rollback/migration impact is addressed when data or schema changes.
|
|
149
|
+
7. No vague instruction survives — "improve UI", "faster", "better UX" without defined behavior fails Clarity.
|
|
150
|
+
|
|
140
151
|
Only flag issues that would cause real problems during implementation planning. Approve unless there are serious gaps that would lead to a flawed plan.
|
|
141
152
|
|
|
142
153
|
### Output
|
|
@@ -18,8 +18,10 @@ reasoning to the parent agent so it can decide whether to re-route.
|
|
|
18
18
|
**In Handoff mode you are running unattended — ask nothing.** You were spawned by an
|
|
19
19
|
orchestrator driving a pipeline; there is no human in your conversation to answer, and a
|
|
20
20
|
question there is silently dropped while the run stalls. Resolve ambiguity in this order:
|
|
21
|
-
the task file → `
|
|
22
|
-
|
|
21
|
+
the task file → the `Spec references` sections of `SPEC.md` → `PLAN.md` → the surrounding
|
|
22
|
+
code's existing patterns → the choice you would recommend. The spec is the contract; if the
|
|
23
|
+
task file and spec disagree, implement the spec and note it in the task's `## Discussion`
|
|
24
|
+
thread. Record what you chose and why in that thread. Only a blocker
|
|
23
25
|
outside the repo (missing credential, unreachable service) justifies reporting `FAIL` early —
|
|
24
26
|
and even then, report it, don't ask about it.
|
|
25
27
|
|
|
@@ -54,6 +54,35 @@ Before writing any path or command into `PLAN.md` or a task file, verify it:
|
|
|
54
54
|
If something cannot be verified, say so in the task's `## Discussion` rather than guessing.
|
|
55
55
|
A stated unknown costs the executor one read; a wrong path costs it a round.
|
|
56
56
|
|
|
57
|
+
## Phase 0 — Legacy sweep (before writing anything)
|
|
58
|
+
|
|
59
|
+
The plan owns ALL unfinished work, not only the new request. Scan and fold in:
|
|
60
|
+
|
|
61
|
+
- `INDEX.md` rows that are not `done`/`cancelled_superseded`.
|
|
62
|
+
- Task files in stale `in_progress` / `blocked` / `changes_requested` /
|
|
63
|
+
`needs_executor_report` / `needs_breakdown` from dead sessions.
|
|
64
|
+
- `docs/AI_HANDOFF/HISTORY.md` + `archive/` — cycles closed with leftovers.
|
|
65
|
+
- `docs/TASKS.md` — `Ready for AI` items are newly-assigned work.
|
|
66
|
+
- `git status` — uncommitted work-in-progress (finish or checkpoint, never drop silently).
|
|
67
|
+
|
|
68
|
+
Every discovered item becomes either a task row in the new plan or an explicitly recorded
|
|
69
|
+
out-of-scope line in PLAN.md §2. Silent omission is a plan defect.
|
|
70
|
+
|
|
71
|
+
**Recovery:** a stuck task record that cannot be cleanly resumed (orphaned worktree,
|
|
72
|
+
contradicting reports, invalid state) is marked `cancelled_superseded` and replaced by
|
|
73
|
+
`TASK-xxx-R1` (`-R2`, …) carrying the same spec references, acceptance criteria and
|
|
74
|
+
verification — link both files' `## Discussion` threads.
|
|
75
|
+
|
|
76
|
+
## Phase 0.5 — Write SPEC.md
|
|
77
|
+
|
|
78
|
+
Write `docs/AI_HANDOFF/SPEC.md` from the template at `templates/docs/AI_HANDOFF/SPEC.md`
|
|
79
|
+
(15 sections). The spec is the contract executors implement against — concrete enough that
|
|
80
|
+
nothing is guessed: exact paths, module and API names, schemas, validation rules,
|
|
81
|
+
permissions, empty/error states, migration behavior, test expectations. Every section is
|
|
82
|
+
filled or marked `N/A — <reason>`; every open question is resolved to a chosen default
|
|
83
|
+
recorded in §14. A vague line ("improve UI", "make it faster" with no number) is a spec
|
|
84
|
+
defect — fix it before writing tasks.
|
|
85
|
+
|
|
57
86
|
## Phase 1 — Write PLAN.md
|
|
58
87
|
|
|
59
88
|
Write all 7 sections to `docs/AI_HANDOFF/PLAN.md`:
|
|
@@ -100,6 +129,7 @@ Use `_TEMPLATE.md` structure (from pre-read context or file).
|
|
|
100
129
|
|
|
101
130
|
| Field | Rule |
|
|
102
131
|
|-------|------|
|
|
132
|
+
| Spec references | SPEC.md section/FR IDs this task implements — every task traces to the spec |
|
|
103
133
|
| Target Files | Exact paths — no two tasks in same wave share a file |
|
|
104
134
|
| Dependencies | `TASK-xxx` or `none` — wave order is inferred from this |
|
|
105
135
|
| Test Cases | Type \| Test Name \| Expected — ≥1 happy + ≥2 edge cases of different kinds |
|
|
@@ -159,6 +189,7 @@ most chains are ordering preferences that a wide wave 1 would satisfy just as we
|
|
|
159
189
|
```
|
|
160
190
|
Cycle: <ID> Date: <YYYY-MM-DD> Base: <current HEAD branch>
|
|
161
191
|
Goal: <1 sentence>
|
|
192
|
+
Spec: docs/AI_HANDOFF/SPEC.md
|
|
162
193
|
Tasks: <N> total
|
|
163
194
|
Status: planning_done — ready for executor
|
|
164
195
|
```
|
|
@@ -178,6 +209,8 @@ catches it:
|
|
|
178
209
|
2. Does every task trace back to something in §1/§6? A task nothing asks for is scope creep — cut it.
|
|
179
210
|
3. Do the tasks together actually deliver §1's success definition, or only the easy part of it? State the gap if there is one.
|
|
180
211
|
4. Is the *unhappy* path planned — errors, empty input, permissions, migration of existing data — or only the feature?
|
|
212
|
+
4b. Does every task carry `Spec references` into SPEC.md, and does every SPEC.md FR trace to at least one task? A spec section no task implements is a silent hole.
|
|
213
|
+
4c. Did the Phase 0 sweep leave anything unplanned — stale tasks, legacy leftovers, Ready-for-AI items, uncommitted WIP — without a §2 out-of-scope line?
|
|
181
214
|
|
|
182
215
|
**Correctness — is anything wrong?**
|
|
183
216
|
5. Every `Target Files` path verified per Grounding? Any `(new)` file marked as such?
|
|
@@ -194,7 +227,7 @@ catches it:
|
|
|
194
227
|
Append the result to `PLAN.md`:
|
|
195
228
|
```
|
|
196
229
|
## Planner Self-Audit
|
|
197
|
-
Checklist:
|
|
230
|
+
Checklist: 14/14 pass
|
|
198
231
|
Fixed during audit: <what you changed, or "nothing">
|
|
199
232
|
Known gaps: <what you deliberately left out and why, or "none">
|
|
200
233
|
```
|
|
@@ -208,6 +241,8 @@ Keep the returned message under 25 lines — the caller may be an orchestrator w
|
|
|
208
241
|
budget is the constraint on the whole run. Detail belongs in `PLAN.md`, not in the reply.
|
|
209
242
|
|
|
210
243
|
- Task count + IDs
|
|
244
|
+
- Spec path + one-line coverage statement (`SPEC.md §5 FR-001→TASK-003`, …)
|
|
245
|
+
- Recovery/superseded pairs (`TASK-007 → TASK-007-R1`) | none
|
|
211
246
|
- Dependency graph (text form: TASK-001 → TASK-003, TASK-002 independent)
|
|
212
247
|
- Wave plan: `wave 1: N tasks | wave 2: M tasks` — flag it if the graph is mostly a chain
|
|
213
248
|
- Self-audit result + any `Known gaps`
|
|
@@ -47,6 +47,7 @@ export const HOOK_EVENT_MAP = {
|
|
|
47
47
|
'sensitive-data-guard.sh',
|
|
48
48
|
'handoff-model-guard.sh',
|
|
49
49
|
'context-hardcap-gate.sh',
|
|
50
|
+
'verification-guard.sh',
|
|
50
51
|
],
|
|
51
52
|
},
|
|
52
53
|
tool_result: {
|
|
@@ -102,6 +103,7 @@ export const FAIL_CLOSED_SCRIPTS = new Set([
|
|
|
102
103
|
|
|
103
104
|
export const ADVISORY_SCRIPTS = new Set([
|
|
104
105
|
'skill-router.sh',
|
|
106
|
+
'verification-guard.sh',
|
|
105
107
|
'record-execution.sh',
|
|
106
108
|
'auto-allow-bash.sh',
|
|
107
109
|
'pre-edit-backup.sh',
|
|
@@ -331,17 +333,122 @@ export { translateExecResult };
|
|
|
331
333
|
// 30s cap that silently disagreed with the runner whenever the env knobs were set.
|
|
332
334
|
const chainExecTimeoutMs = resolveChainExecTimeoutMs;
|
|
333
335
|
|
|
334
|
-
|
|
336
|
+
// BUG-C21-09 (FR-006): hook-errors/ used to append one JSONL row per chain
|
|
337
|
+
// transport error with no byte cap, no rotation, and no sweep — files grew per
|
|
338
|
+
// session forever. Per-file cap mirrors hook-telemetry's <=512KB rotate; the
|
|
339
|
+
// directory sweep is bounded (<= maxEntries stats, <= maxRemovals unlinks) and
|
|
340
|
+
// sampled (~1/16 writes, UKIT_HOOK_ERRORS_SWEEP_PROBABILITY to force/disable).
|
|
341
|
+
const HOOK_ERROR_MAX_BYTES = 512 * 1024;
|
|
342
|
+
const HOOK_ERROR_SWEEP_PROBABILITY_DEFAULT = 1 / 16;
|
|
343
|
+
const HOOK_ERROR_SWEEP_MAX_ENTRIES = 128;
|
|
344
|
+
const HOOK_ERROR_SWEEP_MAX_REMOVALS = 64;
|
|
345
|
+
const HOOK_ERROR_MAX_FILES = 200;
|
|
346
|
+
const HOOK_ERROR_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
347
|
+
|
|
348
|
+
function hookErrorsDirFor(projectRoot) {
|
|
349
|
+
return path.join(projectRoot, '.ukit', 'storage', 'cache', 'hook-errors');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Bounded work on THIS session's file only — identical keep-newest-half shape
|
|
353
|
+
// as hook-telemetry's rotateIfNeeded.
|
|
354
|
+
function rotateHookErrorFileIfNeeded(filePath, incomingBytes, maxBytes) {
|
|
355
|
+
let size = 0;
|
|
356
|
+
try {
|
|
357
|
+
size = fs.statSync(filePath).size;
|
|
358
|
+
} catch {
|
|
359
|
+
return; // first row for this session
|
|
360
|
+
}
|
|
361
|
+
if (size + incomingBytes <= maxBytes) return;
|
|
362
|
+
try {
|
|
363
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
|
|
364
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
365
|
+
const keepBudget = Math.floor(maxBytes / 2);
|
|
366
|
+
const keep = [];
|
|
367
|
+
let kept = 0;
|
|
368
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
369
|
+
const lineBytes = Buffer.byteLength(lines[i], 'utf8') + 1;
|
|
370
|
+
if (kept + lineBytes > keepBudget) break;
|
|
371
|
+
keep.unshift(lines[i]);
|
|
372
|
+
kept += lineBytes;
|
|
373
|
+
}
|
|
374
|
+
fs.writeFileSync(filePath, keep.length ? `${keep.join('\n')}\n` : '', 'utf8');
|
|
375
|
+
} catch {
|
|
376
|
+
// Rotation failed; drop this row rather than grow past the cap.
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// sweepHookErrorsDir(dir, {now, maxAgeMs, maxFiles, maxEntries, maxRemovals}) ->
|
|
381
|
+
// { scanned, removed } — bounded: never scans or removes more than the caps,
|
|
382
|
+
// so a pre-existing oversized dir is amortized down across sampled sweeps.
|
|
383
|
+
function sweepHookErrorsDir(dir, {
|
|
384
|
+
now = Date.now,
|
|
385
|
+
maxAgeMs = HOOK_ERROR_MAX_AGE_MS,
|
|
386
|
+
maxFiles = HOOK_ERROR_MAX_FILES,
|
|
387
|
+
maxEntries = HOOK_ERROR_SWEEP_MAX_ENTRIES,
|
|
388
|
+
maxRemovals = HOOK_ERROR_SWEEP_MAX_REMOVALS,
|
|
389
|
+
} = {}) {
|
|
390
|
+
let names;
|
|
391
|
+
try {
|
|
392
|
+
names = fs.readdirSync(dir);
|
|
393
|
+
} catch {
|
|
394
|
+
return { scanned: 0, removed: 0 };
|
|
395
|
+
}
|
|
396
|
+
const cutoff = now() - maxAgeMs;
|
|
397
|
+
const entries = [];
|
|
398
|
+
let scanned = 0;
|
|
399
|
+
for (const name of names) {
|
|
400
|
+
if (scanned >= maxEntries) break;
|
|
401
|
+
scanned += 1;
|
|
402
|
+
if (!name.endsWith('.jsonl')) continue;
|
|
403
|
+
try {
|
|
404
|
+
const stats = fs.statSync(path.join(dir, name));
|
|
405
|
+
if (stats.isFile()) entries.push({ name, mtimeMs: stats.mtimeMs });
|
|
406
|
+
} catch { /* raced away — fine */ }
|
|
407
|
+
}
|
|
408
|
+
entries.sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
|
|
409
|
+
// Overflow counts eligible FILES only — foreign entries (non-matching names,
|
|
410
|
+
// dirs) inflate `names.length` and would evict real entries while the dir is
|
|
411
|
+
// actually under the cap.
|
|
412
|
+
const overflow = Math.max(0, entries.length - maxFiles);
|
|
413
|
+
let removed = 0;
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
if (removed >= maxRemovals) break;
|
|
416
|
+
if (removed < overflow || entry.mtimeMs < cutoff) {
|
|
417
|
+
try {
|
|
418
|
+
fs.rmSync(path.join(dir, entry.name), { force: true });
|
|
419
|
+
removed += 1;
|
|
420
|
+
} catch { /* raced away — fine */ }
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { scanned, removed };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function hookErrorsSweepProbabilityFromEnv() {
|
|
427
|
+
const raw = Number(process.env.UKIT_HOOK_ERRORS_SWEEP_PROBABILITY);
|
|
428
|
+
if (!Number.isFinite(raw)) return HOOK_ERROR_SWEEP_PROBABILITY_DEFAULT;
|
|
429
|
+
return Math.min(1, Math.max(0, raw));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function recordHookErrorDiagnostic(projectRoot, sessionId, diagnostic, { maxBytes = HOOK_ERROR_MAX_BYTES } = {}) {
|
|
335
433
|
try {
|
|
336
|
-
const dir =
|
|
434
|
+
const dir = hookErrorsDirFor(projectRoot);
|
|
337
435
|
fs.mkdirSync(dir, { recursive: true });
|
|
338
436
|
const safeSession = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'unknown';
|
|
339
|
-
|
|
437
|
+
const filePath = path.join(dir, `${safeSession}.jsonl`);
|
|
438
|
+
const line = `${JSON.stringify(diagnostic)}\n`;
|
|
439
|
+
rotateHookErrorFileIfNeeded(filePath, Buffer.byteLength(line, 'utf8'), maxBytes);
|
|
440
|
+
fs.appendFileSync(filePath, line, 'utf8');
|
|
441
|
+
// Sampled bounded dir sweep — amortizes down any pre-existing oversized dir.
|
|
442
|
+
if (Math.random() < hookErrorsSweepProbabilityFromEnv()) {
|
|
443
|
+
sweepHookErrorsDir(dir);
|
|
444
|
+
}
|
|
340
445
|
} catch {
|
|
341
446
|
// Diagnostics are advisory and must never block or throw.
|
|
342
447
|
}
|
|
343
448
|
}
|
|
344
449
|
|
|
450
|
+
export { recordHookErrorDiagnostic, sweepHookErrorsDir };
|
|
451
|
+
|
|
345
452
|
// Cached across the omp process lifetime: resolution below spawns a probe process, and every
|
|
346
453
|
// tool call would otherwise pay that cost again.
|
|
347
454
|
let cachedNodeExecutable = null;
|
package/templates/AGENTS.md
CHANGED
|
@@ -288,4 +288,18 @@ DuraOne skill chỉ active khi pack `duraone` được cài hoặc `.claude/skil
|
|
|
288
288
|
- No unrelated changes
|
|
289
289
|
- Verification executed and reported
|
|
290
290
|
- Docs updated when source truth changed
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
## Handoff Fullstack Rules
|
|
294
|
+
|
|
295
|
+
- `docs/AI_HANDOFF/RUN.md` là run cursor có thẩm quyền; `Phase:` ≠ `done`/`blocked` nghĩa là run còn sống — Stop gate sẽ từ chối stop và trả về `Next:` step.
|
|
296
|
+
- Recap/checkpoint không bao giờ là completion — chỉ `HANDOFF FULLSTACK COMPLETE` (sau khi `Phase: done`) hoặc `HANDOFF FULLSTACK BLOCKED` (sau khi `Phase: blocked`) mới kết thúc run.
|
|
297
|
+
- Resume tự động mọi task chưa xong: current, legacy, pending, interrupted, recovery (`-R<n>` thay `cancelled_superseded`), và task mới được giao.
|
|
298
|
+
- Handoff-create phải viết `docs/AI_HANDOFF/SPEC.md` chi tiết trước khi tạo task; task nào cũng mang `Spec references`.
|
|
299
|
+
- Kết thúc cycle: docs sync → archive `docs/AI_HANDOFF/archive/cycle-NN/` → `Phase: done` → Final Report có marker.
|
|
300
|
+
- `handoff-clear` bắt buộc đóng RUN.md (`Phase: done` hoặc xóa) — cursor sống sẽ giữ Stop gate chặn session sau.
|
|
301
|
+
|
|
302
|
+
## Compact Instructions
|
|
303
|
+
|
|
304
|
+
Khi compact giữa một handoff run: giữ lại goal, RUN.md path + phase hiện tại, task inventory, task đang làm, `Next:` step, blockers, verification evidence, commits, worktree/copy-back state, và quy tắc "compact không phải completion". Sau compact: đọc lại RUN.md + INDEX.md rồi chạy tiếp `Next:` ngay.
|
|
291
305
|
{{codegraphSection}}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -271,4 +271,18 @@ DuraOne skill chỉ active khi pack `duraone` được cài hoặc `.claude/skil
|
|
|
271
271
|
- No unrelated changes
|
|
272
272
|
- Verification executed and reported
|
|
273
273
|
- Docs updated when source truth changed
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
## Handoff Fullstack Rules
|
|
277
|
+
|
|
278
|
+
- `docs/AI_HANDOFF/RUN.md` là run cursor có thẩm quyền; `Phase:` ≠ `done`/`blocked` nghĩa là run còn sống — Stop gate sẽ từ chối stop và trả về `Next:` step.
|
|
279
|
+
- Recap/checkpoint không bao giờ là completion — chỉ `HANDOFF FULLSTACK COMPLETE` (sau khi `Phase: done`) hoặc `HANDOFF FULLSTACK BLOCKED` (sau khi `Phase: blocked`) mới kết thúc run.
|
|
280
|
+
- Resume tự động mọi task chưa xong: current, legacy, pending, interrupted, recovery (`-R<n>` thay `cancelled_superseded`), và task mới được giao.
|
|
281
|
+
- Handoff-create phải viết `docs/AI_HANDOFF/SPEC.md` chi tiết trước khi tạo task; task nào cũng mang `Spec references`.
|
|
282
|
+
- Kết thúc cycle: docs sync → archive `docs/AI_HANDOFF/archive/cycle-NN/` → `Phase: done` → Final Report có marker.
|
|
283
|
+
- `handoff-clear` bắt buộc đóng RUN.md (`Phase: done` hoặc xóa) — cursor sống sẽ giữ Stop gate chặn session sau.
|
|
284
|
+
|
|
285
|
+
## Compact Instructions
|
|
286
|
+
|
|
287
|
+
Khi compact giữa một handoff run: giữ lại goal, RUN.md path + phase hiện tại, task inventory, task đang làm, `Next:` step, blockers, verification evidence, commits, worktree/copy-back state, và quy tắc "compact không phải completion". Sau compact: đọc lại RUN.md + INDEX.md rồi chạy tiếp `Next:` ngay.
|
|
274
288
|
{{codegraphSection}}
|