@gajae-code/coding-agent 0.3.0 → 0.3.1
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 +18 -0
- package/dist/types/async/job-manager.d.ts +7 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +4 -4
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +1 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
- package/dist/types/hooks/skill-state.d.ts +21 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +143 -11
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +36 -0
- package/src/cli/args.ts +9 -2
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +289 -19
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +22 -4
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +6 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
- package/src/gjc-runtime/ralplan-runtime.ts +213 -36
- package/src/gjc-runtime/state-migrations.ts +54 -7
- package/src/gjc-runtime/state-runtime.ts +461 -64
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-writer.ts +32 -1
- package/src/gjc-runtime/team-runtime.ts +177 -105
- package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
- package/src/gjc-runtime/workflow-manifest.ts +3 -1
- package/src/harness-control-plane/control-endpoint.ts +19 -8
- package/src/harness-control-plane/owner.ts +57 -10
- package/src/harness-control-plane/state-machine.ts +2 -1
- package/src/hooks/skill-state.ts +176 -26
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +61 -18
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/extension-ui-controller.ts +39 -3
- package/src/modes/controllers/input-controller.ts +97 -9
- package/src/modes/controllers/selector-controller.ts +29 -0
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +27 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/tools/subagent.md +12 -7
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +214 -38
- package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
- package/src/skill-state/workflow-state-contract.ts +7 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +29 -5
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +257 -67
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +48 -131
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +48 -7
- package/src/tools/ask.ts +73 -33
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +237 -84
|
@@ -21,10 +21,12 @@ import {
|
|
|
21
21
|
buildWorkflowStateReceipt,
|
|
22
22
|
canonicalWorkflowSkill,
|
|
23
23
|
describeWorkflowStateContract,
|
|
24
|
+
WORKFLOW_STATE_VERSION,
|
|
24
25
|
type WorkflowStateReceipt,
|
|
25
26
|
} from "../skill-state/workflow-state-contract";
|
|
27
|
+
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
26
28
|
import { renderStateGraph, type StateGraphFormat } from "./state-graph";
|
|
27
|
-
import { migrateAndPersistLegacyState } from "./state-migrations";
|
|
29
|
+
import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
|
|
28
30
|
import {
|
|
29
31
|
buildStateStatusSummary,
|
|
30
32
|
compactProjectStateJson,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
detectWorkflowEnvelopeIntegrityMismatch,
|
|
45
47
|
type GenericHardPruneTarget,
|
|
46
48
|
hardPrune,
|
|
49
|
+
readExistingStateForMutation,
|
|
47
50
|
type StateWriterAuditContext,
|
|
48
51
|
softDelete,
|
|
49
52
|
updateWorkflowTransactionJournal,
|
|
@@ -116,6 +119,7 @@ const ACTION_NAMES = new Set([
|
|
|
116
119
|
"gc",
|
|
117
120
|
"migrate",
|
|
118
121
|
"status",
|
|
122
|
+
"doctor",
|
|
119
123
|
]);
|
|
120
124
|
const BOOLEAN_FLAGS = new Set([
|
|
121
125
|
"--json",
|
|
@@ -174,7 +178,18 @@ function assertKnownFlags(args: readonly string[], parsed: ParsedInvocation): vo
|
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
interface ParsedInvocation {
|
|
177
|
-
action:
|
|
181
|
+
action:
|
|
182
|
+
| "read"
|
|
183
|
+
| "write"
|
|
184
|
+
| "clear"
|
|
185
|
+
| "contract"
|
|
186
|
+
| "handoff"
|
|
187
|
+
| "graph"
|
|
188
|
+
| "prune"
|
|
189
|
+
| "gc"
|
|
190
|
+
| "migrate"
|
|
191
|
+
| "status"
|
|
192
|
+
| "doctor";
|
|
178
193
|
positionalSkill?: string;
|
|
179
194
|
}
|
|
180
195
|
|
|
@@ -344,7 +359,8 @@ async function readJsonFile(filePath: string): Promise<Record<string, unknown> |
|
|
|
344
359
|
} catch (error) {
|
|
345
360
|
const err = error as NodeJS.ErrnoException;
|
|
346
361
|
if (err.code === "ENOENT") return null;
|
|
347
|
-
|
|
362
|
+
process.stderr.write(`WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}\n`);
|
|
363
|
+
return null;
|
|
348
364
|
}
|
|
349
365
|
}
|
|
350
366
|
|
|
@@ -354,8 +370,285 @@ async function readJsonValue(filePath: string): Promise<unknown | null> {
|
|
|
354
370
|
} catch (error) {
|
|
355
371
|
const err = error as NodeJS.ErrnoException;
|
|
356
372
|
if (err.code === "ENOENT") return null;
|
|
357
|
-
|
|
373
|
+
process.stderr.write(`WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}\n`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
type DoctorProblemType = "orphan_journal" | "checksum_mismatch" | "schema_violation" | "stale_active_state";
|
|
379
|
+
|
|
380
|
+
interface DoctorProblem {
|
|
381
|
+
type: DoctorProblemType;
|
|
382
|
+
skill?: CanonicalGjcWorkflowSkill;
|
|
383
|
+
path: string;
|
|
384
|
+
message: string;
|
|
385
|
+
fixCommand: string;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
interface DoctorSummary {
|
|
389
|
+
ok: boolean;
|
|
390
|
+
root: string;
|
|
391
|
+
summary: {
|
|
392
|
+
skills_scanned: number;
|
|
393
|
+
files_scanned: number;
|
|
394
|
+
journals_scanned: number;
|
|
395
|
+
findings_total: number;
|
|
396
|
+
by_kind: Record<DoctorProblemType, number>;
|
|
397
|
+
};
|
|
398
|
+
problems: DoctorProblem[];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function readRawJson(filePath: string): Promise<{ exists: boolean; value?: unknown; error?: string }> {
|
|
402
|
+
try {
|
|
403
|
+
return { exists: true, value: JSON.parse(await fs.readFile(filePath, "utf-8")) };
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const err = error as NodeJS.ErrnoException;
|
|
406
|
+
if (err.code === "ENOENT") return { exists: false };
|
|
407
|
+
return { exists: true, error: err.message };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function listJsonFiles(dir: string): Promise<string[]> {
|
|
412
|
+
let entries: string[];
|
|
413
|
+
try {
|
|
414
|
+
entries = await fs.readdir(dir);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
const err = error as NodeJS.ErrnoException;
|
|
417
|
+
if (err.code === "ENOENT") return [];
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
return entries
|
|
421
|
+
.filter(entry => entry.endsWith(".json"))
|
|
422
|
+
.sort()
|
|
423
|
+
.map(entry => path.join(dir, entry));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function doctorProblem(
|
|
427
|
+
type: DoctorProblemType,
|
|
428
|
+
pathValue: string,
|
|
429
|
+
message: string,
|
|
430
|
+
fixCommand: string,
|
|
431
|
+
skill?: CanonicalGjcWorkflowSkill,
|
|
432
|
+
): DoctorProblem {
|
|
433
|
+
return skill
|
|
434
|
+
? { type, skill, path: pathValue, message, fixCommand }
|
|
435
|
+
: { type, path: pathValue, message, fixCommand };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function activeEntryDir(cwd: string, sessionId: string | undefined): string {
|
|
439
|
+
return path.join(stateDirFor(cwd, sessionId), "active");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function skillFromActiveValue(value: unknown): string | undefined {
|
|
443
|
+
return isPlainObject(value) && typeof value.skill === "string" ? value.skill : undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function activeFlag(value: unknown): boolean {
|
|
447
|
+
return isPlainObject(value) && value.active !== false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function collectDoctorSummary(
|
|
451
|
+
cwd: string,
|
|
452
|
+
skill: CanonicalGjcWorkflowSkill | undefined,
|
|
453
|
+
sessionId: string | undefined,
|
|
454
|
+
): Promise<DoctorSummary> {
|
|
455
|
+
const root = path.join(cwd, ".gjc", "state");
|
|
456
|
+
const skills = skill ? [skill] : [...CANONICAL_GJC_WORKFLOW_SKILLS];
|
|
457
|
+
const problems: DoctorProblem[] = [];
|
|
458
|
+
let filesScanned = 0;
|
|
459
|
+
let journalsScanned = 0;
|
|
460
|
+
|
|
461
|
+
for (const currentSkill of skills) {
|
|
462
|
+
const filePath = modeStateFile(cwd, currentSkill, sessionId);
|
|
463
|
+
const raw = await readRawJson(filePath);
|
|
464
|
+
if (!raw.exists) continue;
|
|
465
|
+
filesScanned += 1;
|
|
466
|
+
if (raw.error) {
|
|
467
|
+
problems.push(
|
|
468
|
+
doctorProblem(
|
|
469
|
+
"schema_violation",
|
|
470
|
+
filePath,
|
|
471
|
+
`mode-state JSON is unreadable: ${raw.error}`,
|
|
472
|
+
`gjc state ${currentSkill} migrate`,
|
|
473
|
+
currentSkill,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const validation = validateWorkflowStateEnvelope(currentSkill, raw.value);
|
|
479
|
+
if (!validation.valid) {
|
|
480
|
+
problems.push(
|
|
481
|
+
doctorProblem(
|
|
482
|
+
"schema_violation",
|
|
483
|
+
filePath,
|
|
484
|
+
validation.error ?? `invalid ${currentSkill} state envelope`,
|
|
485
|
+
`gjc state ${currentSkill} migrate`,
|
|
486
|
+
currentSkill,
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const mismatch = await detectWorkflowEnvelopeIntegrityMismatch(filePath);
|
|
491
|
+
if (mismatch) {
|
|
492
|
+
problems.push(
|
|
493
|
+
doctorProblem(
|
|
494
|
+
"checksum_mismatch",
|
|
495
|
+
filePath,
|
|
496
|
+
`expected sha256 ${mismatch.expected} but found ${mismatch.actual}`,
|
|
497
|
+
`gjc state ${currentSkill} migrate`,
|
|
498
|
+
currentSkill,
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const journalFiles = await listJsonFiles(path.join(root, "transactions"));
|
|
505
|
+
for (const journalPath of journalFiles) {
|
|
506
|
+
journalsScanned += 1;
|
|
507
|
+
const raw = await readRawJson(journalPath);
|
|
508
|
+
const value = raw.value;
|
|
509
|
+
const status = isPlainObject(value) && typeof value.status === "string" ? value.status : undefined;
|
|
510
|
+
const paths =
|
|
511
|
+
isPlainObject(value) && Array.isArray(value.paths) ? value.paths.filter(p => typeof p === "string") : [];
|
|
512
|
+
const hasLiveMutation = status === "pending" && paths.some(filePath => path.resolve(filePath).startsWith(root));
|
|
513
|
+
if (!hasLiveMutation) {
|
|
514
|
+
problems.push(
|
|
515
|
+
doctorProblem(
|
|
516
|
+
"orphan_journal",
|
|
517
|
+
journalPath,
|
|
518
|
+
"transaction journal has no matching live mutation",
|
|
519
|
+
"gjc state prune --hard",
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
358
523
|
}
|
|
524
|
+
|
|
525
|
+
const inspectActiveScope = async (scopeSessionId: string | undefined): Promise<void> => {
|
|
526
|
+
const snapshotPath = activeStateFile(cwd, scopeSessionId);
|
|
527
|
+
const snapshot = await readRawJson(snapshotPath);
|
|
528
|
+
if (snapshot.exists) filesScanned += 1;
|
|
529
|
+
const entryFiles = await listJsonFiles(activeEntryDir(cwd, scopeSessionId));
|
|
530
|
+
const entrySkills = new Set<string>();
|
|
531
|
+
for (const entryPath of entryFiles) {
|
|
532
|
+
filesScanned += 1;
|
|
533
|
+
const entry = await readRawJson(entryPath);
|
|
534
|
+
const entrySkill = skillFromActiveValue(entry.value) ?? path.basename(entryPath, ".json");
|
|
535
|
+
entrySkills.add(entrySkill);
|
|
536
|
+
const canonical = canonicalWorkflowSkill(entrySkill);
|
|
537
|
+
if (canonical && !skills.includes(canonical)) continue;
|
|
538
|
+
const statePath = canonical
|
|
539
|
+
? modeStateFile(cwd, canonical, scopeSessionId)
|
|
540
|
+
: path.join(root, `${entrySkill}-state.json`);
|
|
541
|
+
const state = await readRawJson(statePath);
|
|
542
|
+
if (activeFlag(entry.value) && (!state.exists || !activeFlag(state.value))) {
|
|
543
|
+
problems.push(
|
|
544
|
+
doctorProblem(
|
|
545
|
+
"stale_active_state",
|
|
546
|
+
entryPath,
|
|
547
|
+
`active entry for ${entrySkill} does not match a live active mode-state`,
|
|
548
|
+
canonical ? `gjc state ${canonical} clear` : "gjc state prune --hard",
|
|
549
|
+
canonical ?? undefined,
|
|
550
|
+
),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (isPlainObject(snapshot.value)) {
|
|
555
|
+
const activeSkills = Array.isArray(snapshot.value.active_skills) ? snapshot.value.active_skills : [];
|
|
556
|
+
for (const entry of activeSkills) {
|
|
557
|
+
const entrySkill = skillFromActiveValue(entry);
|
|
558
|
+
if (!entrySkill) continue;
|
|
559
|
+
const canonical = canonicalWorkflowSkill(entrySkill);
|
|
560
|
+
if (canonical && !skills.includes(canonical)) continue;
|
|
561
|
+
if (activeFlag(entry) && !entrySkills.has(entrySkill)) {
|
|
562
|
+
problems.push(
|
|
563
|
+
doctorProblem(
|
|
564
|
+
"stale_active_state",
|
|
565
|
+
snapshotPath,
|
|
566
|
+
`active snapshot lists ${entrySkill} but no raw per-skill active entry exists`,
|
|
567
|
+
canonical ? `gjc state ${canonical} clear` : "gjc state prune --hard",
|
|
568
|
+
canonical ?? undefined,
|
|
569
|
+
),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
await inspectActiveScope(sessionId);
|
|
577
|
+
if (!sessionId) {
|
|
578
|
+
const sessionsDir = path.join(root, "sessions");
|
|
579
|
+
let sessions: string[] = [];
|
|
580
|
+
try {
|
|
581
|
+
const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
582
|
+
sessions = entries
|
|
583
|
+
.filter(entry => entry.isDirectory())
|
|
584
|
+
.map(entry => entry.name)
|
|
585
|
+
.sort();
|
|
586
|
+
} catch (error) {
|
|
587
|
+
const err = error as NodeJS.ErrnoException;
|
|
588
|
+
if (err.code !== "ENOENT") throw error;
|
|
589
|
+
}
|
|
590
|
+
for (const rawSession of sessions) await inspectActiveScope(decodeURIComponent(rawSession));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
problems.sort(
|
|
594
|
+
(a, b) =>
|
|
595
|
+
a.type.localeCompare(b.type) || (a.skill ?? "").localeCompare(b.skill ?? "") || a.path.localeCompare(b.path),
|
|
596
|
+
);
|
|
597
|
+
const byKind: Record<DoctorProblemType, number> = {
|
|
598
|
+
orphan_journal: 0,
|
|
599
|
+
checksum_mismatch: 0,
|
|
600
|
+
schema_violation: 0,
|
|
601
|
+
stale_active_state: 0,
|
|
602
|
+
};
|
|
603
|
+
for (const problem of problems) byKind[problem.type] += 1;
|
|
604
|
+
return {
|
|
605
|
+
ok: problems.length === 0,
|
|
606
|
+
root,
|
|
607
|
+
summary: {
|
|
608
|
+
skills_scanned: skills.length,
|
|
609
|
+
files_scanned: filesScanned,
|
|
610
|
+
journals_scanned: journalsScanned,
|
|
611
|
+
findings_total: problems.length,
|
|
612
|
+
by_kind: byKind,
|
|
613
|
+
},
|
|
614
|
+
problems,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function renderDoctorText(summary: DoctorSummary): string {
|
|
619
|
+
const lines = [
|
|
620
|
+
`ok: ${summary.ok}`,
|
|
621
|
+
`root: ${summary.root}`,
|
|
622
|
+
`skills_scanned: ${summary.summary.skills_scanned}`,
|
|
623
|
+
`files_scanned: ${summary.summary.files_scanned}`,
|
|
624
|
+
`journals_scanned: ${summary.summary.journals_scanned}`,
|
|
625
|
+
`findings_total: ${summary.summary.findings_total}`,
|
|
626
|
+
`counts: ${Object.entries(summary.summary.by_kind)
|
|
627
|
+
.map(([kind, count]) => `${kind}=${count}`)
|
|
628
|
+
.join(", ")}`,
|
|
629
|
+
];
|
|
630
|
+
for (const problem of summary.problems) {
|
|
631
|
+
lines.push(
|
|
632
|
+
`finding: kind=${problem.type} skill=${problem.skill ?? "-"} path=${problem.path} message=${problem.message} fix=${problem.fixCommand}`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
return `${lines.join("\n")}\n`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function handleDoctor(
|
|
639
|
+
args: readonly string[],
|
|
640
|
+
cwd: string,
|
|
641
|
+
positionalSkill: string | undefined,
|
|
642
|
+
): Promise<StateCommandResult> {
|
|
643
|
+
const rawSkill = flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim();
|
|
644
|
+
if (rawSkill) assertKnownMode(rawSkill);
|
|
645
|
+
const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
|
|
646
|
+
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
647
|
+
const summary = await collectDoctorSummary(cwd, rawSkill as CanonicalGjcWorkflowSkill | undefined, sessionId);
|
|
648
|
+
return {
|
|
649
|
+
status: summary.ok ? 0 : 1,
|
|
650
|
+
stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderDoctorText(summary),
|
|
651
|
+
};
|
|
359
652
|
}
|
|
360
653
|
|
|
361
654
|
async function warnAndAuditOutOfBandIfNeeded(
|
|
@@ -364,7 +657,14 @@ async function warnAndAuditOutOfBandIfNeeded(
|
|
|
364
657
|
skill: CanonicalGjcWorkflowSkill,
|
|
365
658
|
options?: { mutationId?: string; forced?: boolean },
|
|
366
659
|
): Promise<string | undefined> {
|
|
367
|
-
|
|
660
|
+
let mismatch: Awaited<ReturnType<typeof detectWorkflowEnvelopeIntegrityMismatch>>;
|
|
661
|
+
try {
|
|
662
|
+
mismatch = await detectWorkflowEnvelopeIntegrityMismatch(filePath);
|
|
663
|
+
} catch {
|
|
664
|
+
// Unparseable/corrupt state has no recoverable checksum to compare; the strict
|
|
665
|
+
// mutation reader already gates unforced overwrites, so fail-open here.
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
368
668
|
if (!mismatch) return undefined;
|
|
369
669
|
const message = `WARNING: workflow mode-state out-of-band edit detected for ${skill}: ${filePath} expected sha256 ${mismatch.expected} but found ${mismatch.actual}`;
|
|
370
670
|
await appendAuditEntry(cwd, {
|
|
@@ -394,7 +694,7 @@ async function writeJsonAtomic(
|
|
|
394
694
|
fromPhase?: string;
|
|
395
695
|
toPhase?: string;
|
|
396
696
|
},
|
|
397
|
-
): Promise<string
|
|
697
|
+
): Promise<{ warning?: string; stamped: Record<string, unknown> }> {
|
|
398
698
|
const warning = options?.skill
|
|
399
699
|
? await warnAndAuditOutOfBandIfNeeded(cwd, filePath, options.skill, {
|
|
400
700
|
mutationId: options.mutationId,
|
|
@@ -417,7 +717,7 @@ async function writeJsonAtomic(
|
|
|
417
717
|
forced: options?.force ?? false,
|
|
418
718
|
},
|
|
419
719
|
});
|
|
420
|
-
return warning;
|
|
720
|
+
return { warning, stamped: (await readJsonFile(filePath)) ?? {} };
|
|
421
721
|
}
|
|
422
722
|
|
|
423
723
|
function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
|
|
@@ -752,8 +1052,15 @@ async function handleWrite(
|
|
|
752
1052
|
);
|
|
753
1053
|
|
|
754
1054
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
755
|
-
const
|
|
756
|
-
const
|
|
1055
|
+
const forced = hasFlag(args, "--force");
|
|
1056
|
+
const existingRead = await readExistingStateForMutation(filePath);
|
|
1057
|
+
if (existingRead.kind === "corrupt" && !forced) {
|
|
1058
|
+
throw new StateCommandError(
|
|
1059
|
+
2,
|
|
1060
|
+
`existing state for ${mode} is corrupt or tampered (${existingRead.error}); use --force to overwrite`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
|
|
757
1064
|
const nowIsoStr = nowIso();
|
|
758
1065
|
const mutationId = `${mode}:${nowIsoStr}`;
|
|
759
1066
|
const receipt = buildWorkflowStateReceipt({
|
|
@@ -765,10 +1072,6 @@ async function handleWrite(
|
|
|
765
1072
|
nowIso: nowIsoStr,
|
|
766
1073
|
mutationId,
|
|
767
1074
|
});
|
|
768
|
-
if (existingRaw !== null && !isPlainObject(existingRaw)) {
|
|
769
|
-
throw new StateCommandError(2, `existing state for ${mode} must be a JSON object before write`);
|
|
770
|
-
}
|
|
771
|
-
const existingPayload = existing ?? {};
|
|
772
1075
|
const innerState = (payload.state as Record<string, unknown> | undefined) ?? {};
|
|
773
1076
|
const incomingPhase =
|
|
774
1077
|
typeof payload.current_phase === "string" && payload.current_phase.trim()
|
|
@@ -797,19 +1100,26 @@ async function handleWrite(
|
|
|
797
1100
|
merged.skill = mode;
|
|
798
1101
|
if (incomingPhase) {
|
|
799
1102
|
merged.current_phase = incomingPhase;
|
|
800
|
-
} else if (typeof merged.current_phase !== "string") {
|
|
801
|
-
|
|
802
|
-
typeof existingPayload.current_phase === "string" ? existingPayload.current_phase : "
|
|
1103
|
+
} else if (typeof merged.current_phase !== "string" || !merged.current_phase.trim()) {
|
|
1104
|
+
const retainedPhase =
|
|
1105
|
+
typeof existingPayload.current_phase === "string" ? existingPayload.current_phase.trim() : "";
|
|
1106
|
+
merged.current_phase = retainedPhase || initialPhaseForSkill(mode);
|
|
1107
|
+
} else {
|
|
1108
|
+
merged.current_phase = merged.current_phase.trim();
|
|
803
1109
|
}
|
|
804
|
-
|
|
1110
|
+
merged.version = WORKFLOW_STATE_VERSION;
|
|
805
1111
|
if (typeof merged.active !== "boolean") merged.active = true;
|
|
806
1112
|
merged.updated_at = nowIsoStr;
|
|
807
1113
|
merged.receipt = receipt;
|
|
808
1114
|
if (sessionId && typeof merged.session_id !== "string") merged.session_id = sessionId;
|
|
809
1115
|
|
|
810
|
-
const fromPhase =
|
|
811
|
-
|
|
812
|
-
const
|
|
1116
|
+
const fromPhase =
|
|
1117
|
+
typeof existingPayload.current_phase === "string" ? existingPayload.current_phase.trim() : undefined;
|
|
1118
|
+
const toPhase = merged.current_phase as string;
|
|
1119
|
+
const manifestStates = new Set(getSkillManifest(mode).states.map(state => state.id));
|
|
1120
|
+
if (!manifestStates.has(toPhase) && !forced) {
|
|
1121
|
+
throw new StateCommandError(2, `unknown ${mode} phase "${toPhase}"; use --force to bypass`);
|
|
1122
|
+
}
|
|
813
1123
|
if (fromPhase && toPhase && isKnownWorkflowState(mode, fromPhase) && isKnownWorkflowState(mode, toPhase)) {
|
|
814
1124
|
if (!isValidTransition(mode, fromPhase, toPhase) && !forced) {
|
|
815
1125
|
throw new StateCommandError(
|
|
@@ -822,13 +1132,14 @@ async function handleWrite(
|
|
|
822
1132
|
const validation = validateWorkflowStateEnvelope(mode, merged);
|
|
823
1133
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
824
1134
|
|
|
825
|
-
const outOfBandWarning = await writeJsonAtomic(cwd, filePath, merged, "write", {
|
|
1135
|
+
const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, merged, "write", {
|
|
826
1136
|
skill: mode,
|
|
827
1137
|
mutationId,
|
|
828
1138
|
force: forced,
|
|
829
1139
|
fromPhase,
|
|
830
1140
|
toPhase,
|
|
831
1141
|
});
|
|
1142
|
+
const stampedReceipt = isPlainObject(stamped.receipt) ? stamped.receipt : {};
|
|
832
1143
|
|
|
833
1144
|
const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
|
|
834
1145
|
const active = merged.active !== false;
|
|
@@ -836,7 +1147,16 @@ async function handleWrite(
|
|
|
836
1147
|
|
|
837
1148
|
return {
|
|
838
1149
|
status: 0,
|
|
839
|
-
stdout:
|
|
1150
|
+
stdout: renderCliWriteReceipt({
|
|
1151
|
+
ok: true,
|
|
1152
|
+
skill: mode,
|
|
1153
|
+
state_path: filePath,
|
|
1154
|
+
current_phase: phase,
|
|
1155
|
+
active,
|
|
1156
|
+
mutation_id: typeof stampedReceipt.mutation_id === "string" ? stampedReceipt.mutation_id : mutationId,
|
|
1157
|
+
status: typeof stampedReceipt.status === "string" ? stampedReceipt.status : undefined,
|
|
1158
|
+
content_sha256: stampedReceipt.content_sha256,
|
|
1159
|
+
}),
|
|
840
1160
|
...(outOfBandWarning ? { stderr: `${outOfBandWarning}\n` } : {}),
|
|
841
1161
|
};
|
|
842
1162
|
}
|
|
@@ -856,19 +1176,44 @@ async function handleClear(
|
|
|
856
1176
|
);
|
|
857
1177
|
|
|
858
1178
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
859
|
-
const
|
|
1179
|
+
const forced = hasFlag(args, "--force");
|
|
1180
|
+
const existingRead = await readExistingStateForMutation(filePath);
|
|
1181
|
+
if (existingRead.kind === "corrupt" && !forced) {
|
|
1182
|
+
throw new StateCommandError(
|
|
1183
|
+
2,
|
|
1184
|
+
`existing state for ${mode} is corrupt or tampered (${existingRead.error}); use --force to overwrite`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const existing = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1188
|
+
const clearedAt = nowIso();
|
|
860
1189
|
const cleared: Record<string, unknown> = {
|
|
1190
|
+
skill: mode,
|
|
861
1191
|
...existing,
|
|
862
1192
|
active: false,
|
|
863
1193
|
current_phase: "complete",
|
|
864
|
-
updated_at:
|
|
1194
|
+
updated_at: clearedAt,
|
|
1195
|
+
version: WORKFLOW_STATE_VERSION,
|
|
865
1196
|
};
|
|
866
|
-
|
|
1197
|
+
cleared.skill = mode;
|
|
1198
|
+
const mutationId = `${mode}:clear:${clearedAt}`;
|
|
1199
|
+
const receipt = buildWorkflowStateReceipt({
|
|
1200
|
+
cwd,
|
|
1201
|
+
skill: mode,
|
|
1202
|
+
owner: "gjc-state-cli",
|
|
1203
|
+
command: `gjc state ${mode} clear`,
|
|
1204
|
+
sessionId,
|
|
1205
|
+
nowIso: clearedAt,
|
|
1206
|
+
mutationId,
|
|
1207
|
+
});
|
|
1208
|
+
cleared.receipt = receipt;
|
|
1209
|
+
const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, cleared, "clear", {
|
|
867
1210
|
skill: mode,
|
|
868
|
-
|
|
1211
|
+
mutationId,
|
|
1212
|
+
force: forced,
|
|
869
1213
|
fromPhase: typeof existing.current_phase === "string" ? existing.current_phase : undefined,
|
|
870
1214
|
toPhase: "complete",
|
|
871
1215
|
});
|
|
1216
|
+
const stampedReceipt = isPlainObject(stamped.receipt) ? stamped.receipt : {};
|
|
872
1217
|
|
|
873
1218
|
await syncWorkflowSkillState({
|
|
874
1219
|
cwd,
|
|
@@ -882,7 +1227,16 @@ async function handleClear(
|
|
|
882
1227
|
});
|
|
883
1228
|
return {
|
|
884
1229
|
status: 0,
|
|
885
|
-
stdout:
|
|
1230
|
+
stdout: renderCliWriteReceipt({
|
|
1231
|
+
ok: true,
|
|
1232
|
+
skill: mode,
|
|
1233
|
+
state_path: filePath,
|
|
1234
|
+
active: false,
|
|
1235
|
+
current_phase: typeof cleared.current_phase === "string" ? cleared.current_phase : undefined,
|
|
1236
|
+
mutation_id: typeof stampedReceipt.mutation_id === "string" ? stampedReceipt.mutation_id : mutationId,
|
|
1237
|
+
status: typeof stampedReceipt.status === "string" ? stampedReceipt.status : undefined,
|
|
1238
|
+
content_sha256: stampedReceipt.content_sha256,
|
|
1239
|
+
}),
|
|
886
1240
|
...(outOfBandWarning ? { stderr: `${outOfBandWarning}\n` } : {}),
|
|
887
1241
|
};
|
|
888
1242
|
}
|
|
@@ -932,14 +1286,29 @@ async function handleHandoff(
|
|
|
932
1286
|
|
|
933
1287
|
const callerPath = modeStateFile(cwd, caller, sessionId);
|
|
934
1288
|
const calleePath = modeStateFile(cwd, callee, sessionId);
|
|
935
|
-
const
|
|
936
|
-
|
|
1289
|
+
const forced = hasFlag(args, "--force");
|
|
1290
|
+
const callerRead = await readExistingStateForMutation(callerPath);
|
|
1291
|
+
if (callerRead.kind === "corrupt" && !forced) {
|
|
1292
|
+
throw new StateCommandError(
|
|
1293
|
+
2,
|
|
1294
|
+
`existing state for ${caller} is corrupt or tampered (${callerRead.error}); use --force to overwrite`,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (callerRead.kind === "absent") {
|
|
937
1298
|
throw new StateCommandError(
|
|
938
1299
|
2,
|
|
939
1300
|
`gjc state ${caller} handoff: caller is not active (no mode-state file at ${callerPath})`,
|
|
940
1301
|
);
|
|
941
1302
|
}
|
|
942
|
-
const
|
|
1303
|
+
const calleeRead = await readExistingStateForMutation(calleePath);
|
|
1304
|
+
if (calleeRead.kind === "corrupt" && !forced) {
|
|
1305
|
+
throw new StateCommandError(
|
|
1306
|
+
2,
|
|
1307
|
+
`existing state for ${callee} is corrupt or tampered (${calleeRead.error}); use --force to overwrite`,
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const existingCaller = callerRead.kind === "valid" ? callerRead.value : {};
|
|
1311
|
+
const existingCallee = calleeRead.kind === "valid" ? calleeRead.value : {};
|
|
943
1312
|
|
|
944
1313
|
const handoffAt = nowIso();
|
|
945
1314
|
const mutationId = `${caller}:handoff:${callee}:${handoffAt}`;
|
|
@@ -963,10 +1332,12 @@ async function handleHandoff(
|
|
|
963
1332
|
});
|
|
964
1333
|
|
|
965
1334
|
const calleeInitial = initialPhaseForSkill(callee);
|
|
1335
|
+
const normalizedCaller = migrateWorkflowState(existingCaller, caller).state;
|
|
1336
|
+
const normalizedCallee = migrateWorkflowState(existingCallee, callee).state;
|
|
966
1337
|
const mergedCalleeState: Record<string, unknown> = {
|
|
967
|
-
...
|
|
1338
|
+
...normalizedCallee,
|
|
968
1339
|
skill: callee,
|
|
969
|
-
version:
|
|
1340
|
+
version: WORKFLOW_STATE_VERSION,
|
|
970
1341
|
active: true,
|
|
971
1342
|
current_phase: calleeInitial,
|
|
972
1343
|
handoff_from: caller,
|
|
@@ -978,8 +1349,9 @@ async function handleHandoff(
|
|
|
978
1349
|
mergedCalleeState.session_id = sessionId;
|
|
979
1350
|
}
|
|
980
1351
|
const mergedCallerState: Record<string, unknown> = {
|
|
981
|
-
...
|
|
1352
|
+
...normalizedCaller,
|
|
982
1353
|
skill: caller,
|
|
1354
|
+
version: WORKFLOW_STATE_VERSION,
|
|
983
1355
|
active: false,
|
|
984
1356
|
current_phase: "handoff",
|
|
985
1357
|
handoff_to: callee,
|
|
@@ -1004,26 +1376,29 @@ async function handleHandoff(
|
|
|
1004
1376
|
// root aggregate. strict:true on the active-state read tolerates ENOENT
|
|
1005
1377
|
// only; corrupt JSON / IO failures propagate as non-zero CLI status.
|
|
1006
1378
|
const force = hasFlag(args, "--force");
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1379
|
+
const calleeWrite = await writeJsonAtomic(cwd, calleePath, mergedCalleeState, "handoff", {
|
|
1380
|
+
skill: callee,
|
|
1381
|
+
mutationId,
|
|
1382
|
+
force,
|
|
1383
|
+
fromPhase: typeof existingCallee.current_phase === "string" ? existingCallee.current_phase : undefined,
|
|
1384
|
+
toPhase: calleeInitial,
|
|
1385
|
+
});
|
|
1386
|
+
await updateWorkflowTransactionJournal(cwd, mutationId, { steps: ["callee-mode-state"] });
|
|
1387
|
+
const callerWrite = await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
|
|
1388
|
+
skill: caller,
|
|
1389
|
+
mutationId,
|
|
1390
|
+
force,
|
|
1391
|
+
fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
|
|
1392
|
+
toPhase: "handoff",
|
|
1393
|
+
});
|
|
1394
|
+
await updateWorkflowTransactionJournal(cwd, mutationId, {
|
|
1395
|
+
steps: ["callee-mode-state", "caller-mode-state"],
|
|
1396
|
+
});
|
|
1397
|
+
const warnings = [calleeWrite.warning, callerWrite.warning].filter(
|
|
1398
|
+
(warning): warning is string => typeof warning === "string",
|
|
1399
|
+
);
|
|
1400
|
+
const stampedCallerReceipt = isPlainObject(callerWrite.stamped.receipt) ? callerWrite.stamped.receipt : {};
|
|
1401
|
+
const stampedCalleeReceipt = isPlainObject(calleeWrite.stamped.receipt) ? calleeWrite.stamped.receipt : {};
|
|
1027
1402
|
for (const warning of warnings) process.stderr.write(`${warning}\n`);
|
|
1028
1403
|
if (process.env.GJC_STATE_HANDOFF_FAIL_AFTER_CALLER === mutationId) {
|
|
1029
1404
|
throw new StateCommandError(1, `injected handoff failure after caller write for ${mutationId}`);
|
|
@@ -1068,17 +1443,33 @@ async function handleHandoff(
|
|
|
1068
1443
|
|
|
1069
1444
|
return {
|
|
1070
1445
|
status: 0,
|
|
1071
|
-
stdout:
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1446
|
+
stdout: renderCliWriteReceipt({
|
|
1447
|
+
ok: true,
|
|
1448
|
+
from: caller,
|
|
1449
|
+
to: callee,
|
|
1450
|
+
handoff_at: handoffAt,
|
|
1451
|
+
phases: {
|
|
1452
|
+
from: mergedCallerState.current_phase,
|
|
1453
|
+
to: mergedCalleeState.current_phase,
|
|
1078
1454
|
},
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1455
|
+
receipts: {
|
|
1456
|
+
from: {
|
|
1457
|
+
mutation_id: stampedCallerReceipt.mutation_id,
|
|
1458
|
+
status: stampedCallerReceipt.status,
|
|
1459
|
+
content_sha256: stampedCallerReceipt.content_sha256,
|
|
1460
|
+
},
|
|
1461
|
+
to: {
|
|
1462
|
+
mutation_id: stampedCalleeReceipt.mutation_id,
|
|
1463
|
+
status: stampedCalleeReceipt.status,
|
|
1464
|
+
content_sha256: stampedCalleeReceipt.content_sha256,
|
|
1465
|
+
},
|
|
1466
|
+
},
|
|
1467
|
+
paths: {
|
|
1468
|
+
from: callerPath,
|
|
1469
|
+
to: calleePath,
|
|
1470
|
+
active_state: activeStateFile(cwd, sessionId),
|
|
1471
|
+
},
|
|
1472
|
+
}),
|
|
1082
1473
|
};
|
|
1083
1474
|
}
|
|
1084
1475
|
|
|
@@ -1392,9 +1783,13 @@ async function handleMigrate(
|
|
|
1392
1783
|
);
|
|
1393
1784
|
}
|
|
1394
1785
|
const filePath = modeStateFile(cwd, mode, selectors.sessionId);
|
|
1786
|
+
const forced = hasFlag(args, "--force");
|
|
1395
1787
|
const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, filePath, mode, {
|
|
1396
|
-
forced
|
|
1788
|
+
forced,
|
|
1397
1789
|
});
|
|
1790
|
+
if (mismatchWarning && !forced) {
|
|
1791
|
+
throw new StateCommandError(2, `${mismatchWarning}; use --force to migrate tampered mode-state`);
|
|
1792
|
+
}
|
|
1398
1793
|
const result = await migrateAndPersistLegacyState({
|
|
1399
1794
|
cwd,
|
|
1400
1795
|
skill: mode,
|
|
@@ -1424,6 +1819,8 @@ export async function runNativeStateCommand(args: string[], cwd = process.cwd())
|
|
|
1424
1819
|
return await handleContract(args, cwd, parsed.positionalSkill);
|
|
1425
1820
|
case "status":
|
|
1426
1821
|
return await handleStatus(args, cwd, parsed.positionalSkill);
|
|
1822
|
+
case "doctor":
|
|
1823
|
+
return await handleDoctor(args, cwd, parsed.positionalSkill);
|
|
1427
1824
|
case "handoff":
|
|
1428
1825
|
return await handleHandoff(args, cwd, parsed.positionalSkill);
|
|
1429
1826
|
case "graph":
|