@sayknow-cli/coding-agent 0.2.4 → 0.2.5
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/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/lsp/startup-events.d.ts +1 -0
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/skc-runtime/session-layout.d.ts +59 -0
- package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/skc-runtime/state-graph.d.ts +1 -1
- package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/skc-runtime/state-schema.d.ts +2 -0
- package/dist/types/skc-runtime/state-writer.d.ts +36 -7
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/package.json +7 -7
- package/scripts/build-binary.ts +0 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/setup-cli.ts +14 -1
- package/src/cli/update-cli.ts +53 -3
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/launch.ts +1 -1
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-registry.ts +9 -2
- package/src/config/model-resolver.ts +13 -2
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +17 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
- package/src/defaults/skc/skills/team/SKILL.md +51 -47
- package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
- package/src/exec/bash-executor.ts +3 -1
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/skc-plugins/injection.ts +23 -4
- package/src/extensibility/skc-plugins/state.ts +16 -1
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/startup-events.ts +24 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/welcome.ts +20 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +27 -19
- package/src/modes/prompt-action-autocomplete.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +7 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/session/agent-session.ts +43 -41
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/skc-runtime/deep-interview-recorder.ts +51 -18
- package/src/skc-runtime/deep-interview-runtime.ts +49 -23
- package/src/skc-runtime/goal-mode-request.ts +26 -11
- package/src/skc-runtime/launch-tmux.ts +68 -15
- package/src/skc-runtime/ralplan-runtime.ts +79 -50
- package/src/skc-runtime/session-layout.ts +180 -0
- package/src/skc-runtime/session-resolution.ts +217 -0
- package/src/skc-runtime/state-graph.ts +1 -2
- package/src/skc-runtime/state-migrations.ts +1 -0
- package/src/skc-runtime/state-runtime.ts +237 -114
- package/src/skc-runtime/state-schema.ts +2 -0
- package/src/skc-runtime/state-writer.ts +310 -42
- package/src/skc-runtime/team-runtime.ts +43 -19
- package/src/skc-runtime/tmux-sessions.ts +43 -2
- package/src/skc-runtime/ultragoal-guard.ts +45 -2
- package/src/skc-runtime/ultragoal-runtime.ts +121 -41
- package/src/skc-runtime/workflow-command-ref.ts +1 -2
- package/src/skc-runtime/workflow-manifest.ts +1 -2
- package/src/skill-state/active-state.ts +116 -129
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
- package/src/task/agents.ts +1 -22
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +34 -12
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/computer.ts +58 -4
- package/src/utils/edit-mode.ts +1 -1
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
|
@@ -27,6 +27,13 @@ import {
|
|
|
27
27
|
} from "../skill-state/workflow-state-contract";
|
|
28
28
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
29
29
|
import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
|
|
30
|
+
import { activeSnapshotPath, auditPath, modeStatePath, sessionStateDir } from "./session-layout";
|
|
31
|
+
import {
|
|
32
|
+
resolveSkcSessionForRead,
|
|
33
|
+
resolveSkcSessionForWrite,
|
|
34
|
+
SessionResolutionError,
|
|
35
|
+
writeSessionActivityMarker,
|
|
36
|
+
} from "./session-resolution";
|
|
30
37
|
import { renderStateGraph, type StateGraphFormat } from "./state-graph";
|
|
31
38
|
import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
|
|
32
39
|
import {
|
|
@@ -53,16 +60,16 @@ import {
|
|
|
53
60
|
softDelete,
|
|
54
61
|
updateWorkflowTransactionJournal,
|
|
55
62
|
type WorkflowEnvelopeIntegrityMismatch,
|
|
56
|
-
|
|
63
|
+
writeGuardedWorkflowEnvelopeAtomic,
|
|
57
64
|
} from "./state-writer";
|
|
58
65
|
import { getSkillManifest, isKnownWorkflowState, isValidTransition, typedArgsFor } from "./workflow-manifest";
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
68
|
* Native implementation of the `skc state read|write|clear` command surface.
|
|
62
69
|
*
|
|
63
|
-
* Simple file-receipt operations against
|
|
64
|
-
* `.skc/
|
|
65
|
-
*
|
|
70
|
+
* Simple file-receipt operations against session-scoped state under
|
|
71
|
+
* `.skc/_session-{id}/state/`. This is the sanctioned CLI mediator for
|
|
72
|
+
* mutation-guarded SKC state — agents call it instead of editing those files directly.
|
|
66
73
|
*/
|
|
67
74
|
|
|
68
75
|
export interface StateCommandResult {
|
|
@@ -72,6 +79,7 @@ export interface StateCommandResult {
|
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
82
|
+
const TERMINAL_CLEAR_PHASES = new Set(["complete", "completed", "cancelled", "canceled", "failed"]);
|
|
75
83
|
const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
76
84
|
const KNOWN_MODES: readonly string[] = CANONICAL_SKC_WORKFLOW_SKILLS;
|
|
77
85
|
|
|
@@ -269,16 +277,22 @@ async function readInputJson(value: string | undefined, cwd: string): Promise<Re
|
|
|
269
277
|
|
|
270
278
|
interface ResolvedSelectors {
|
|
271
279
|
mode: CanonicalSkcWorkflowSkill | undefined;
|
|
272
|
-
|
|
280
|
+
skcSessionId: string;
|
|
273
281
|
threadId: string | undefined;
|
|
274
282
|
turnId: string | undefined;
|
|
275
283
|
payload: Record<string, unknown> | undefined;
|
|
276
284
|
}
|
|
277
285
|
|
|
286
|
+
// `clear` resolves like a read (explicit -> payload -> env -> latest-activity marker)
|
|
287
|
+
// per the spec: read/status/clear may fall back to the most-recent session. Commands
|
|
288
|
+
// that create or mutate new state roots still require an explicit/env session id.
|
|
289
|
+
const WRITE_SESSION_ACTIONS = new Set<ParsedInvocation["action"]>(["write", "handoff", "prune", "migrate"]);
|
|
290
|
+
|
|
278
291
|
async function resolveSelectors(
|
|
279
292
|
args: readonly string[],
|
|
280
293
|
cwd: string,
|
|
281
294
|
positionalSkill: string | undefined,
|
|
295
|
+
action: ParsedInvocation["action"],
|
|
282
296
|
): Promise<ResolvedSelectors> {
|
|
283
297
|
const payload = await readInputJson(flagValue(args, "--input"), cwd);
|
|
284
298
|
|
|
@@ -297,33 +311,32 @@ async function resolveSelectors(
|
|
|
297
311
|
}
|
|
298
312
|
if (mode) assertKnownMode(mode);
|
|
299
313
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
sessionId = payload.session_id.trim() || undefined;
|
|
309
|
-
}
|
|
310
|
-
if (!sessionId && explicitSessionId === undefined) {
|
|
311
|
-
const envSessionId = process.env.SKC_SESSION_ID?.trim();
|
|
312
|
-
if (envSessionId) sessionId = envSessionId;
|
|
313
|
-
}
|
|
314
|
-
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
314
|
+
const sessionSources = {
|
|
315
|
+
flagValue: flagValue(args, "--session-id"),
|
|
316
|
+
payloadSessionId: payload?.session_id,
|
|
317
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
318
|
+
};
|
|
319
|
+
const session = WRITE_SESSION_ACTIONS.has(action)
|
|
320
|
+
? resolveSkcSessionForWrite(cwd, sessionSources)
|
|
321
|
+
: await resolveSkcSessionForRead(cwd, sessionSources);
|
|
315
322
|
|
|
316
323
|
const threadId = flagValue(args, "--thread-id")?.trim() || undefined;
|
|
317
324
|
if (threadId) assertSafePathComponent(threadId, "thread-id");
|
|
318
325
|
const turnId = flagValue(args, "--turn-id")?.trim() || undefined;
|
|
319
326
|
if (turnId) assertSafePathComponent(turnId, "turn-id");
|
|
320
327
|
|
|
321
|
-
return {
|
|
328
|
+
return {
|
|
329
|
+
mode: mode as CanonicalSkcWorkflowSkill | undefined,
|
|
330
|
+
skcSessionId: session.skcSessionId,
|
|
331
|
+
threadId,
|
|
332
|
+
turnId,
|
|
333
|
+
payload,
|
|
334
|
+
};
|
|
322
335
|
}
|
|
323
336
|
|
|
324
337
|
async function inferModeFromActiveState(
|
|
325
338
|
cwd: string,
|
|
326
|
-
sessionId: string
|
|
339
|
+
sessionId: string,
|
|
327
340
|
): Promise<CanonicalSkcWorkflowSkill | undefined> {
|
|
328
341
|
const state = await readVisibleSkillActiveState(cwd, sessionId);
|
|
329
342
|
const entries = listActiveSkills(state);
|
|
@@ -333,22 +346,53 @@ async function inferModeFromActiveState(
|
|
|
333
346
|
return canonical ?? undefined;
|
|
334
347
|
}
|
|
335
348
|
|
|
336
|
-
function
|
|
337
|
-
return
|
|
349
|
+
function stateDirFor(cwd: string, sessionId: string): string {
|
|
350
|
+
return sessionStateDir(cwd, sessionId);
|
|
338
351
|
}
|
|
339
352
|
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
if (!sessionId) return base;
|
|
343
|
-
return path.join(base, "sessions", encodeSessionSegment(sessionId));
|
|
353
|
+
function modeStateFile(cwd: string, mode: string, sessionId: string): string {
|
|
354
|
+
return modeStatePath(cwd, sessionId, mode);
|
|
344
355
|
}
|
|
345
356
|
|
|
346
|
-
function
|
|
347
|
-
return
|
|
357
|
+
function activeStateFile(cwd: string, sessionId: string): string {
|
|
358
|
+
return activeSnapshotPath(cwd, sessionId);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function stateRelativePath(cwd: string, filePath: string): string {
|
|
362
|
+
return path.relative(cwd, filePath).split(path.sep).join(path.posix.sep);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function touchStateActivityMarker(cwd: string, sessionId: string, filePath: string): Promise<void> {
|
|
366
|
+
await writeSessionActivityMarker(cwd, sessionId, {
|
|
367
|
+
writer: "state-runtime",
|
|
368
|
+
path: stateRelativePath(cwd, filePath),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function readActivePhaseForSkill(
|
|
373
|
+
cwd: string,
|
|
374
|
+
sessionId: string,
|
|
375
|
+
mode: CanonicalSkcWorkflowSkill,
|
|
376
|
+
): Promise<string | undefined> {
|
|
377
|
+
const state = await readVisibleSkillActiveState(cwd, sessionId);
|
|
378
|
+
const entries = listActiveSkills(state);
|
|
379
|
+
const entry = entries.find(item => item.skill === mode) ?? (state?.skill === mode ? state : undefined);
|
|
380
|
+
return isPlainObject(entry) && typeof entry.phase === "string" ? entry.phase.trim() || undefined : undefined;
|
|
348
381
|
}
|
|
349
382
|
|
|
350
|
-
function
|
|
351
|
-
|
|
383
|
+
async function describeStaleClearState(
|
|
384
|
+
cwd: string,
|
|
385
|
+
sessionId: string,
|
|
386
|
+
mode: CanonicalSkcWorkflowSkill,
|
|
387
|
+
existing: Record<string, unknown>,
|
|
388
|
+
): Promise<string | undefined> {
|
|
389
|
+
const phase = typeof existing.current_phase === "string" ? existing.current_phase.trim() : undefined;
|
|
390
|
+
if (phase && TERMINAL_CLEAR_PHASES.has(phase)) return `mode-state is already terminal (${phase})`;
|
|
391
|
+
const activePhase = await readActivePhaseForSkill(cwd, sessionId, mode);
|
|
392
|
+
if (activePhase && phase && activePhase !== phase) {
|
|
393
|
+
return `active-state phase ${activePhase} differs from mode-state phase ${phase}`;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
352
396
|
}
|
|
353
397
|
|
|
354
398
|
async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
|
|
@@ -438,7 +482,7 @@ function doctorProblem(
|
|
|
438
482
|
: { type, path: pathValue, message, fixCommand };
|
|
439
483
|
}
|
|
440
484
|
|
|
441
|
-
function activeEntryDir(cwd: string, sessionId: string
|
|
485
|
+
function activeEntryDir(cwd: string, sessionId: string): string {
|
|
442
486
|
return path.join(stateDirFor(cwd, sessionId), "active");
|
|
443
487
|
}
|
|
444
488
|
|
|
@@ -499,9 +543,9 @@ function pushPhaseDriftProblem(options: {
|
|
|
499
543
|
async function collectDoctorSummary(
|
|
500
544
|
cwd: string,
|
|
501
545
|
skill: CanonicalSkcWorkflowSkill | undefined,
|
|
502
|
-
sessionId: string
|
|
546
|
+
sessionId: string,
|
|
503
547
|
): Promise<DoctorSummary> {
|
|
504
|
-
const root =
|
|
548
|
+
const root = sessionStateDir(cwd, sessionId);
|
|
505
549
|
const skills = skill ? [skill] : [...CANONICAL_SKC_WORKFLOW_SKILLS];
|
|
506
550
|
const problems: DoctorProblem[] = [];
|
|
507
551
|
let filesScanned = 0;
|
|
@@ -575,7 +619,7 @@ async function collectDoctorSummary(
|
|
|
575
619
|
}
|
|
576
620
|
}
|
|
577
621
|
|
|
578
|
-
const inspectActiveScope = async (scopeSessionId: string
|
|
622
|
+
const inspectActiveScope = async (scopeSessionId: string): Promise<void> => {
|
|
579
623
|
const snapshotPath = activeStateFile(cwd, scopeSessionId);
|
|
580
624
|
const snapshot = await readRawJson(snapshotPath);
|
|
581
625
|
if (snapshot.exists) filesScanned += 1;
|
|
@@ -616,7 +660,9 @@ async function collectDoctorSummary(
|
|
|
616
660
|
}
|
|
617
661
|
}
|
|
618
662
|
if (isPlainObject(snapshot.value)) {
|
|
619
|
-
const activeSkills = Array.isArray(snapshot.value.active_skills)
|
|
663
|
+
const activeSkills: unknown[] = Array.isArray(snapshot.value.active_skills)
|
|
664
|
+
? snapshot.value.active_skills
|
|
665
|
+
: [];
|
|
620
666
|
for (const entry of activeSkills) {
|
|
621
667
|
const entrySkill = skillFromActiveValue(entry);
|
|
622
668
|
if (!entrySkill) continue;
|
|
@@ -650,21 +696,6 @@ async function collectDoctorSummary(
|
|
|
650
696
|
};
|
|
651
697
|
|
|
652
698
|
await inspectActiveScope(sessionId);
|
|
653
|
-
if (!sessionId) {
|
|
654
|
-
const sessionsDir = path.join(root, "sessions");
|
|
655
|
-
let sessions: string[] = [];
|
|
656
|
-
try {
|
|
657
|
-
const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
658
|
-
sessions = entries
|
|
659
|
-
.filter(entry => entry.isDirectory())
|
|
660
|
-
.map(entry => entry.name)
|
|
661
|
-
.sort();
|
|
662
|
-
} catch (error) {
|
|
663
|
-
const err = error as NodeJS.ErrnoException;
|
|
664
|
-
if (err.code !== "ENOENT") throw error;
|
|
665
|
-
}
|
|
666
|
-
for (const rawSession of sessions) await inspectActiveScope(decodeURIComponent(rawSession));
|
|
667
|
-
}
|
|
668
699
|
|
|
669
700
|
problems.sort(
|
|
670
701
|
(a, b) =>
|
|
@@ -718,9 +749,17 @@ async function handleDoctor(
|
|
|
718
749
|
): Promise<StateCommandResult> {
|
|
719
750
|
const rawSkill = flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim();
|
|
720
751
|
if (rawSkill) assertKnownMode(rawSkill);
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
752
|
+
const payload = await readInputJson(flagValue(args, "--input"), cwd);
|
|
753
|
+
const session = await resolveSkcSessionForRead(cwd, {
|
|
754
|
+
flagValue: flagValue(args, "--session-id"),
|
|
755
|
+
payloadSessionId: payload?.session_id,
|
|
756
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
757
|
+
});
|
|
758
|
+
const summary = await collectDoctorSummary(
|
|
759
|
+
cwd,
|
|
760
|
+
rawSkill as CanonicalSkcWorkflowSkill | undefined,
|
|
761
|
+
session.skcSessionId,
|
|
762
|
+
);
|
|
724
763
|
return {
|
|
725
764
|
status: summary.ok ? 0 : 1,
|
|
726
765
|
stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderDoctorText(summary),
|
|
@@ -729,6 +768,7 @@ async function handleDoctor(
|
|
|
729
768
|
|
|
730
769
|
async function warnAndAuditOutOfBandIfNeeded(
|
|
731
770
|
cwd: string,
|
|
771
|
+
sessionId: string,
|
|
732
772
|
filePath: string,
|
|
733
773
|
skill: CanonicalSkcWorkflowSkill,
|
|
734
774
|
options?: { mutationId?: string; forced?: boolean },
|
|
@@ -743,7 +783,7 @@ async function warnAndAuditOutOfBandIfNeeded(
|
|
|
743
783
|
}
|
|
744
784
|
if (!mismatch) return undefined;
|
|
745
785
|
const message = `WARNING: workflow mode-state out-of-band edit detected for ${skill}: ${filePath} expected sha256 ${mismatch.expected} but found ${mismatch.actual}`;
|
|
746
|
-
await appendAuditEntry(cwd, {
|
|
786
|
+
await appendAuditEntry(cwd, sessionId, {
|
|
747
787
|
ts: new Date().toISOString(),
|
|
748
788
|
skill,
|
|
749
789
|
category: "state",
|
|
@@ -758,12 +798,19 @@ async function warnAndAuditOutOfBandIfNeeded(
|
|
|
758
798
|
return message;
|
|
759
799
|
}
|
|
760
800
|
|
|
801
|
+
function existingStateRevision(value: unknown): number | undefined {
|
|
802
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
803
|
+
const revision = (value as Record<string, unknown>).state_revision;
|
|
804
|
+
return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
|
|
805
|
+
}
|
|
806
|
+
|
|
761
807
|
async function writeJsonAtomic(
|
|
762
808
|
cwd: string,
|
|
763
809
|
filePath: string,
|
|
764
810
|
value: unknown,
|
|
765
811
|
verb: "write" | "clear" | "handoff" | "reconcile" = "write",
|
|
766
812
|
options?: {
|
|
813
|
+
sessionId: string;
|
|
767
814
|
skill?: CanonicalSkcWorkflowSkill;
|
|
768
815
|
mutationId?: string;
|
|
769
816
|
force?: boolean;
|
|
@@ -773,7 +820,7 @@ async function writeJsonAtomic(
|
|
|
773
820
|
},
|
|
774
821
|
): Promise<{ warning?: string; stamped: Record<string, unknown> }> {
|
|
775
822
|
const warning = options?.skill
|
|
776
|
-
? await warnAndAuditOutOfBandIfNeeded(cwd, filePath, options.skill, {
|
|
823
|
+
? await warnAndAuditOutOfBandIfNeeded(cwd, options.sessionId, filePath, options.skill, {
|
|
777
824
|
mutationId: options.mutationId,
|
|
778
825
|
forced: options.force ?? false,
|
|
779
826
|
})
|
|
@@ -781,9 +828,15 @@ async function writeJsonAtomic(
|
|
|
781
828
|
if (warning && !options?.force) {
|
|
782
829
|
throw new StateCommandError(2, `${warning}; use --force to overwrite tampered mode-state`);
|
|
783
830
|
}
|
|
784
|
-
|
|
831
|
+
// Authoritative CLI/runtime write. Stamp the next state_revision under the
|
|
832
|
+
// writer lock; do not enforce an optimistic `expectedRevision` here (tamper
|
|
833
|
+
// detection is handled by warnAndAuditOutOfBandIfNeeded above, and a forced
|
|
834
|
+
// write must succeed over corrupt/missing prior state).
|
|
835
|
+
await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
|
|
785
836
|
cwd,
|
|
837
|
+
policy: "source",
|
|
786
838
|
audit: {
|
|
839
|
+
sessionId: options?.sessionId ?? "",
|
|
787
840
|
category: "state",
|
|
788
841
|
verb,
|
|
789
842
|
owner: options?.owner ?? "skc-state-cli",
|
|
@@ -843,13 +896,14 @@ function parseSinceFlag(args: readonly string[]): string | undefined {
|
|
|
843
896
|
async function readAuditWindow(
|
|
844
897
|
cwd: string,
|
|
845
898
|
args: readonly string[],
|
|
899
|
+
sessionId: string,
|
|
846
900
|
): Promise<{ entries: unknown[]; limit: number; since?: string; truncated: boolean }> {
|
|
847
901
|
const limit = parseLimitFlag(args);
|
|
848
902
|
const since = parseSinceFlag(args);
|
|
849
|
-
const
|
|
903
|
+
const auditFile = auditPath(cwd, sessionId);
|
|
850
904
|
let raw = "";
|
|
851
905
|
try {
|
|
852
|
-
raw = await fs.readFile(
|
|
906
|
+
raw = await fs.readFile(auditFile, "utf-8");
|
|
853
907
|
} catch (error) {
|
|
854
908
|
const err = error as NodeJS.ErrnoException;
|
|
855
909
|
if (err.code !== "ENOENT") throw error;
|
|
@@ -957,6 +1011,14 @@ function buildHudForMode(
|
|
|
957
1011
|
typeof (rawLedger as Record<string, unknown>).timestamp === "string"
|
|
958
1012
|
? ((rawLedger as Record<string, unknown>).timestamp as string)
|
|
959
1013
|
: undefined,
|
|
1014
|
+
kind:
|
|
1015
|
+
typeof (rawLedger as Record<string, unknown>).kind === "string"
|
|
1016
|
+
? ((rawLedger as Record<string, unknown>).kind as string)
|
|
1017
|
+
: undefined,
|
|
1018
|
+
evidence:
|
|
1019
|
+
typeof (rawLedger as Record<string, unknown>).evidence === "string"
|
|
1020
|
+
? ((rawLedger as Record<string, unknown>).evidence as string)
|
|
1021
|
+
: undefined,
|
|
960
1022
|
}
|
|
961
1023
|
: undefined;
|
|
962
1024
|
const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
|
|
@@ -1006,7 +1068,7 @@ function buildHudForMode(
|
|
|
1006
1068
|
async function syncWorkflowSkillState(options: {
|
|
1007
1069
|
cwd: string;
|
|
1008
1070
|
mode: CanonicalSkcWorkflowSkill;
|
|
1009
|
-
sessionId: string
|
|
1071
|
+
sessionId: string;
|
|
1010
1072
|
threadId?: string;
|
|
1011
1073
|
turnId?: string;
|
|
1012
1074
|
active: boolean;
|
|
@@ -1026,6 +1088,7 @@ async function syncWorkflowSkillState(options: {
|
|
|
1026
1088
|
source: "skc-state-cli",
|
|
1027
1089
|
hud: buildHudForMode(options.mode, options.payload),
|
|
1028
1090
|
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
1091
|
+
sourceRevision: existingStateRevision(options.payload),
|
|
1029
1092
|
});
|
|
1030
1093
|
} catch {
|
|
1031
1094
|
// HUD sync is best-effort and must not change command semantics.
|
|
@@ -1045,14 +1108,19 @@ async function syncWorkflowSkillState(options: {
|
|
|
1045
1108
|
export async function reconcileWorkflowSkillState(options: {
|
|
1046
1109
|
cwd: string;
|
|
1047
1110
|
mode: CanonicalSkcWorkflowSkill;
|
|
1048
|
-
sessionId
|
|
1111
|
+
sessionId?: string;
|
|
1049
1112
|
threadId?: string;
|
|
1050
1113
|
turnId?: string;
|
|
1051
1114
|
active: boolean;
|
|
1052
1115
|
phase: string;
|
|
1053
1116
|
payload: Record<string, unknown>;
|
|
1117
|
+
sourceRevision?: number;
|
|
1054
1118
|
}): Promise<{ stateFile: string }> {
|
|
1055
|
-
const { cwd, mode,
|
|
1119
|
+
const { cwd, mode, threadId, turnId, active, payload } = options;
|
|
1120
|
+
const { skcSessionId: sessionId } = resolveSkcSessionForWrite(cwd, {
|
|
1121
|
+
payloadSessionId: options.sessionId,
|
|
1122
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
1123
|
+
});
|
|
1056
1124
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
1057
1125
|
const existingRead = await readExistingStateForMutation(filePath);
|
|
1058
1126
|
const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
|
|
@@ -1096,14 +1164,37 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1096
1164
|
const validation = validateWorkflowStateEnvelope(mode, merged);
|
|
1097
1165
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
1098
1166
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1167
|
+
if (existingRead.kind === "corrupt") await fs.rm(filePath, { force: true });
|
|
1168
|
+
await writeGuardedWorkflowEnvelopeAtomic(filePath, merged, {
|
|
1169
|
+
cwd,
|
|
1170
|
+
policy: "source",
|
|
1171
|
+
receipt: {
|
|
1172
|
+
cwd,
|
|
1173
|
+
skill: mode,
|
|
1174
|
+
owner: "skc-runtime",
|
|
1175
|
+
command: `skc ${mode} (reconcile)`,
|
|
1176
|
+
sessionId,
|
|
1177
|
+
nowIso: nowIsoStr,
|
|
1178
|
+
mutationId,
|
|
1179
|
+
verb: "reconcile",
|
|
1180
|
+
forced: true,
|
|
1181
|
+
fromPhase,
|
|
1182
|
+
toPhase: trimmedPhase,
|
|
1183
|
+
},
|
|
1184
|
+
audit: {
|
|
1185
|
+
category: "state",
|
|
1186
|
+
verb: "reconcile",
|
|
1187
|
+
owner: "skc-runtime",
|
|
1188
|
+
sessionId,
|
|
1189
|
+
skill: mode,
|
|
1190
|
+
mutationId,
|
|
1191
|
+
forced: true,
|
|
1192
|
+
fromPhase,
|
|
1193
|
+
toPhase: trimmedPhase,
|
|
1194
|
+
},
|
|
1106
1195
|
});
|
|
1196
|
+
const persisted = (await readJsonFile(filePath)) ?? {};
|
|
1197
|
+
const sourceRevision = options.sourceRevision ?? existingStateRevision(persisted);
|
|
1107
1198
|
|
|
1108
1199
|
// Reconciliation drives the active-state/HUD update directly (not via the
|
|
1109
1200
|
// best-effort syncWorkflowSkillState wrapper) so a failed HUD/active-state write
|
|
@@ -1120,7 +1211,9 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1120
1211
|
source: "skc-runtime-reconcile",
|
|
1121
1212
|
hud: buildHudForMode(mode, merged),
|
|
1122
1213
|
receipt,
|
|
1214
|
+
sourceRevision,
|
|
1123
1215
|
});
|
|
1216
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1124
1217
|
return { stateFile: filePath };
|
|
1125
1218
|
}
|
|
1126
1219
|
export async function readWorkflowStateJson(
|
|
@@ -1128,7 +1221,11 @@ export async function readWorkflowStateJson(
|
|
|
1128
1221
|
skill: CanonicalSkcWorkflowSkill,
|
|
1129
1222
|
sessionId?: string,
|
|
1130
1223
|
): Promise<Record<string, unknown>> {
|
|
1131
|
-
|
|
1224
|
+
const session = await resolveSkcSessionForRead(cwd, {
|
|
1225
|
+
payloadSessionId: sessionId,
|
|
1226
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
1227
|
+
});
|
|
1228
|
+
return (await readJsonFile(modeStateFile(cwd, skill, session.skcSessionId))) ?? {};
|
|
1132
1229
|
}
|
|
1133
1230
|
|
|
1134
1231
|
async function handleRead(
|
|
@@ -1136,12 +1233,12 @@ async function handleRead(
|
|
|
1136
1233
|
cwd: string,
|
|
1137
1234
|
positionalSkill: string | undefined,
|
|
1138
1235
|
): Promise<StateCommandResult> {
|
|
1139
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1140
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
1236
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1237
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
|
|
1141
1238
|
const fields = parseFieldsFlag(args);
|
|
1142
1239
|
if (mode) {
|
|
1143
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
1144
|
-
const existing = await readWorkflowStateJson(cwd, mode, selectors.
|
|
1240
|
+
const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
|
|
1241
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.skcSessionId);
|
|
1145
1242
|
const envelope = { skill: mode, state: existing, storage_path: filePath };
|
|
1146
1243
|
const manifest = getSkillManifest(mode);
|
|
1147
1244
|
if (fields) {
|
|
@@ -1169,7 +1266,7 @@ async function handleRead(
|
|
|
1169
1266
|
: renderStateMarkdown(mode, envelope, manifest),
|
|
1170
1267
|
};
|
|
1171
1268
|
}
|
|
1172
|
-
const filePath = activeStateFile(cwd, selectors.
|
|
1269
|
+
const filePath = activeStateFile(cwd, selectors.skcSessionId);
|
|
1173
1270
|
const existingRaw = await readJsonValue(filePath);
|
|
1174
1271
|
const existing = isPlainObject(existingRaw) ? existingRaw : null;
|
|
1175
1272
|
return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
|
|
@@ -1180,16 +1277,16 @@ async function handleStatus(
|
|
|
1180
1277
|
cwd: string,
|
|
1181
1278
|
positionalSkill: string | undefined,
|
|
1182
1279
|
): Promise<StateCommandResult> {
|
|
1183
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1184
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
1280
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1281
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
|
|
1185
1282
|
if (!mode) {
|
|
1186
1283
|
throw new StateCommandError(
|
|
1187
1284
|
2,
|
|
1188
|
-
"skc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1285
|
+
"skc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1189
1286
|
);
|
|
1190
1287
|
}
|
|
1191
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
1192
|
-
const existing = await readWorkflowStateJson(cwd, mode, selectors.
|
|
1288
|
+
const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
|
|
1289
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.skcSessionId);
|
|
1193
1290
|
const summary = buildStateStatusSummary(
|
|
1194
1291
|
mode,
|
|
1195
1292
|
{ skill: mode, state: existing, storage_path: filePath },
|
|
@@ -1207,14 +1304,14 @@ async function handleWrite(
|
|
|
1207
1304
|
cwd: string,
|
|
1208
1305
|
positionalSkill: string | undefined,
|
|
1209
1306
|
): Promise<StateCommandResult> {
|
|
1210
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1211
|
-
const { sessionId, threadId, turnId, payload } = selectors;
|
|
1307
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "write");
|
|
1308
|
+
const { skcSessionId: sessionId, threadId, turnId, payload } = selectors;
|
|
1212
1309
|
if (!payload) throw new StateCommandError(2, "skc state write requires --input '<json>'");
|
|
1213
1310
|
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1214
1311
|
if (!mode)
|
|
1215
1312
|
throw new StateCommandError(
|
|
1216
1313
|
2,
|
|
1217
|
-
"skc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1314
|
+
"skc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1218
1315
|
);
|
|
1219
1316
|
|
|
1220
1317
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
@@ -1303,6 +1400,7 @@ async function handleWrite(
|
|
|
1303
1400
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
1304
1401
|
|
|
1305
1402
|
const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, merged, "write", {
|
|
1403
|
+
sessionId,
|
|
1306
1404
|
skill: mode,
|
|
1307
1405
|
mutationId,
|
|
1308
1406
|
force: forced,
|
|
@@ -1313,7 +1411,14 @@ async function handleWrite(
|
|
|
1313
1411
|
|
|
1314
1412
|
const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
|
|
1315
1413
|
const active = merged.active !== false;
|
|
1414
|
+
// Reflect the freshly stamped mode-state revision onto the in-memory payload so the
|
|
1415
|
+
// active-state/HUD sync derives a `sourceRevision` from the persisted revision, not the
|
|
1416
|
+
// stale pre-write value; otherwise the active-state writer stale-skips the update and the
|
|
1417
|
+
// mirror keeps the prior phase (e.g. staying "interviewing" after a "handoff" write).
|
|
1418
|
+
const stampedRevision = existingStateRevision(stamped);
|
|
1419
|
+
if (typeof stampedRevision === "number") merged.state_revision = stampedRevision;
|
|
1316
1420
|
await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
|
|
1421
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1317
1422
|
|
|
1318
1423
|
return {
|
|
1319
1424
|
status: 0,
|
|
@@ -1336,13 +1441,13 @@ async function handleClear(
|
|
|
1336
1441
|
cwd: string,
|
|
1337
1442
|
positionalSkill: string | undefined,
|
|
1338
1443
|
): Promise<StateCommandResult> {
|
|
1339
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1340
|
-
const { sessionId, threadId, turnId } = selectors;
|
|
1444
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "clear");
|
|
1445
|
+
const { skcSessionId: sessionId, threadId, turnId } = selectors;
|
|
1341
1446
|
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1342
1447
|
if (!mode)
|
|
1343
1448
|
throw new StateCommandError(
|
|
1344
1449
|
2,
|
|
1345
|
-
"skc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1450
|
+
"skc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1346
1451
|
);
|
|
1347
1452
|
|
|
1348
1453
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
@@ -1355,6 +1460,10 @@ async function handleClear(
|
|
|
1355
1460
|
);
|
|
1356
1461
|
}
|
|
1357
1462
|
const existing = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1463
|
+
const staleReason = await describeStaleClearState(cwd, sessionId, mode, existing);
|
|
1464
|
+
if (staleReason && !forced) {
|
|
1465
|
+
throw new StateCommandError(2, `existing state for ${mode} is stale (${staleReason}); use --force to clear`);
|
|
1466
|
+
}
|
|
1358
1467
|
const clearedAt = nowIso();
|
|
1359
1468
|
const cleared: Record<string, unknown> = {
|
|
1360
1469
|
skill: mode,
|
|
@@ -1377,6 +1486,7 @@ async function handleClear(
|
|
|
1377
1486
|
});
|
|
1378
1487
|
cleared.receipt = receipt;
|
|
1379
1488
|
const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, cleared, "clear", {
|
|
1489
|
+
sessionId,
|
|
1380
1490
|
skill: mode,
|
|
1381
1491
|
mutationId,
|
|
1382
1492
|
force: forced,
|
|
@@ -1395,6 +1505,7 @@ async function handleClear(
|
|
|
1395
1505
|
phase: "complete",
|
|
1396
1506
|
payload: cleared,
|
|
1397
1507
|
});
|
|
1508
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1398
1509
|
return {
|
|
1399
1510
|
status: 0,
|
|
1400
1511
|
stdout: renderCliWriteReceipt({
|
|
@@ -1435,13 +1546,13 @@ async function handleHandoff(
|
|
|
1435
1546
|
cwd: string,
|
|
1436
1547
|
positionalSkill: string | undefined,
|
|
1437
1548
|
): Promise<StateCommandResult> {
|
|
1438
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1439
|
-
const { sessionId, threadId, turnId } = selectors;
|
|
1549
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
|
|
1550
|
+
const { skcSessionId: sessionId, threadId, turnId } = selectors;
|
|
1440
1551
|
const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1441
1552
|
if (!caller) {
|
|
1442
1553
|
throw new StateCommandError(
|
|
1443
1554
|
2,
|
|
1444
|
-
"skc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in
|
|
1555
|
+
"skc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in the current session active state",
|
|
1445
1556
|
);
|
|
1446
1557
|
}
|
|
1447
1558
|
const calleeRaw = flagValue(args, "--to")?.trim();
|
|
@@ -1544,6 +1655,7 @@ async function handleHandoff(
|
|
|
1544
1655
|
|
|
1545
1656
|
await beginWorkflowTransactionJournal({
|
|
1546
1657
|
cwd,
|
|
1658
|
+
sessionId,
|
|
1547
1659
|
mutationId,
|
|
1548
1660
|
caller,
|
|
1549
1661
|
callee,
|
|
@@ -1559,21 +1671,23 @@ async function handleHandoff(
|
|
|
1559
1671
|
// only; corrupt JSON / IO failures propagate as non-zero CLI status.
|
|
1560
1672
|
const force = hasFlag(args, "--force");
|
|
1561
1673
|
const calleeWrite = await writeJsonAtomic(cwd, calleePath, mergedCalleeState, "handoff", {
|
|
1674
|
+
sessionId,
|
|
1562
1675
|
skill: callee,
|
|
1563
1676
|
mutationId,
|
|
1564
1677
|
force,
|
|
1565
1678
|
fromPhase: typeof existingCallee.current_phase === "string" ? existingCallee.current_phase : undefined,
|
|
1566
1679
|
toPhase: calleeInitial,
|
|
1567
1680
|
});
|
|
1568
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, { steps: ["callee-mode-state"] });
|
|
1681
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["callee-mode-state"] });
|
|
1569
1682
|
const callerWrite = await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
|
|
1683
|
+
sessionId,
|
|
1570
1684
|
skill: caller,
|
|
1571
1685
|
mutationId,
|
|
1572
1686
|
force,
|
|
1573
1687
|
fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
|
|
1574
1688
|
toPhase: "handoff",
|
|
1575
1689
|
});
|
|
1576
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, {
|
|
1690
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
|
|
1577
1691
|
steps: ["callee-mode-state", "caller-mode-state"],
|
|
1578
1692
|
});
|
|
1579
1693
|
const warnings = [calleeWrite.warning, callerWrite.warning].filter(
|
|
@@ -1618,10 +1732,11 @@ async function handleHandoff(
|
|
|
1618
1732
|
receipt: calleeReceipt,
|
|
1619
1733
|
},
|
|
1620
1734
|
});
|
|
1621
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, {
|
|
1735
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
|
|
1622
1736
|
steps: ["callee-mode-state", "caller-mode-state", "active-state"],
|
|
1623
1737
|
});
|
|
1624
|
-
await completeWorkflowTransactionJournal(cwd, mutationId);
|
|
1738
|
+
await completeWorkflowTransactionJournal(cwd, sessionId, mutationId);
|
|
1739
|
+
await touchStateActivityMarker(cwd, sessionId, callerPath);
|
|
1625
1740
|
|
|
1626
1741
|
return {
|
|
1627
1742
|
status: 0,
|
|
@@ -1660,7 +1775,7 @@ async function handleContract(
|
|
|
1660
1775
|
cwd: string,
|
|
1661
1776
|
positionalSkill: string | undefined,
|
|
1662
1777
|
): Promise<StateCommandResult> {
|
|
1663
|
-
const { mode } = await resolveSelectors(args, cwd, positionalSkill);
|
|
1778
|
+
const { mode } = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1664
1779
|
if (!mode) {
|
|
1665
1780
|
throw new StateCommandError(2, "skc state contract requires --mode <skill>, positional <skill>, or input.skill");
|
|
1666
1781
|
}
|
|
@@ -1715,11 +1830,7 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
|
|
|
1715
1830
|
if (normalized === "audit.jsonl") return undefined;
|
|
1716
1831
|
if (normalized === SKILL_ACTIVE_STATE_FILE || normalized.endsWith(`/${SKILL_ACTIVE_STATE_FILE}`)) return undefined;
|
|
1717
1832
|
if (normalized.startsWith("active/") || normalized.includes("/active/")) return undefined;
|
|
1718
|
-
if (
|
|
1719
|
-
/^[^/]+-state\.json$/.test(normalized) ||
|
|
1720
|
-
(normalized.includes("/sessions/") && /\/[^/]+-state\.json$/.test(normalized))
|
|
1721
|
-
)
|
|
1722
|
-
return undefined;
|
|
1833
|
+
if (/^[^/]+-state\.json$/.test(normalized) || false) return undefined;
|
|
1723
1834
|
if (normalized.startsWith("artifacts/") || normalized.includes("/artifacts/")) return "artifact";
|
|
1724
1835
|
if (
|
|
1725
1836
|
normalized.startsWith("logs/") ||
|
|
@@ -1745,9 +1856,10 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
|
|
|
1745
1856
|
|
|
1746
1857
|
async function collectRetentionCandidates(
|
|
1747
1858
|
cwd: string,
|
|
1859
|
+
sessionId: string,
|
|
1748
1860
|
skills: readonly CanonicalSkcWorkflowSkill[],
|
|
1749
1861
|
): Promise<RetentionCandidate[]> {
|
|
1750
|
-
const stateRoot =
|
|
1862
|
+
const stateRoot = sessionStateDir(cwd, sessionId);
|
|
1751
1863
|
const policies = new Map<string, { keep?: number; maxAgeDays?: number }>();
|
|
1752
1864
|
for (const skill of skills) {
|
|
1753
1865
|
for (const policy of getSkillManifest(skill).retention) {
|
|
@@ -1828,7 +1940,11 @@ async function buildGcSummary(
|
|
|
1828
1940
|
flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim() || "all";
|
|
1829
1941
|
if (rawSkill !== "all") assertKnownMode(rawSkill);
|
|
1830
1942
|
const skills = rawSkill === "all" ? CANONICAL_SKC_WORKFLOW_SKILLS : [rawSkill as CanonicalSkcWorkflowSkill];
|
|
1831
|
-
const
|
|
1943
|
+
const session = await resolveSkcSessionForRead(cwd, {
|
|
1944
|
+
flagValue: flagValue(args, "--session-id"),
|
|
1945
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
1946
|
+
});
|
|
1947
|
+
const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, session.skcSessionId, skills));
|
|
1832
1948
|
const counts: Record<string, number> = {};
|
|
1833
1949
|
for (const candidate of eligible) counts[candidate.category] = (counts[candidate.category] ?? 0) + 1;
|
|
1834
1950
|
const targets: GenericHardPruneTarget[] = eligible.map(candidate => ({
|
|
@@ -1842,6 +1958,7 @@ async function buildGcSummary(
|
|
|
1842
1958
|
cwd,
|
|
1843
1959
|
audit: {
|
|
1844
1960
|
cwd,
|
|
1961
|
+
sessionId: session.skcSessionId,
|
|
1845
1962
|
skill: rawSkill,
|
|
1846
1963
|
category: "prune",
|
|
1847
1964
|
verb: "gc",
|
|
@@ -1853,7 +1970,7 @@ async function buildGcSummary(
|
|
|
1853
1970
|
skill: rawSkill as CanonicalSkcWorkflowSkill | "all",
|
|
1854
1971
|
dry_run: dryRun,
|
|
1855
1972
|
eligible: eligible.map(candidate => candidate.relativePath),
|
|
1856
|
-
pruned: pruned.map(filePath => path.relative(
|
|
1973
|
+
pruned: pruned.map(filePath => path.relative(sessionStateDir(cwd, session.skcSessionId), filePath)),
|
|
1857
1974
|
counts,
|
|
1858
1975
|
};
|
|
1859
1976
|
}
|
|
@@ -1864,7 +1981,11 @@ async function handleGraph(
|
|
|
1864
1981
|
positionalSkill: string | undefined,
|
|
1865
1982
|
): Promise<StateCommandResult> {
|
|
1866
1983
|
if (hasFlag(args, "--history")) {
|
|
1867
|
-
const
|
|
1984
|
+
const session = await resolveSkcSessionForRead(_cwd, {
|
|
1985
|
+
flagValue: flagValue(args, "--session-id"),
|
|
1986
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
1987
|
+
});
|
|
1988
|
+
const history = await readAuditWindow(_cwd, args, session.skcSessionId);
|
|
1868
1989
|
return {
|
|
1869
1990
|
status: 0,
|
|
1870
1991
|
stdout: hasFlag(args, "--json") ? `${JSON.stringify(history, null, 2)}\n` : renderHistoryMarkdown(history),
|
|
@@ -1887,20 +2008,21 @@ async function handlePrune(
|
|
|
1887
2008
|
cwd: string,
|
|
1888
2009
|
positionalSkill: string | undefined,
|
|
1889
2010
|
): Promise<StateCommandResult> {
|
|
1890
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1891
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
2011
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "prune");
|
|
2012
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
|
|
1892
2013
|
if (!mode) {
|
|
1893
2014
|
throw new StateCommandError(
|
|
1894
2015
|
2,
|
|
1895
|
-
"skc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
2016
|
+
"skc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1896
2017
|
);
|
|
1897
2018
|
}
|
|
1898
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
2019
|
+
const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
|
|
1899
2020
|
const olderThanDays = parseNonNegativeIntegerFlag(args, "--older-than");
|
|
1900
2021
|
const status = flagValue(args, "--status")?.trim();
|
|
1901
2022
|
const targets: GenericHardPruneTarget[] = [{ path: filePath, category: "prune" }];
|
|
1902
2023
|
const audit: StateWriterAuditContext = {
|
|
1903
2024
|
cwd,
|
|
2025
|
+
sessionId: selectors.skcSessionId,
|
|
1904
2026
|
skill: mode,
|
|
1905
2027
|
category: "prune",
|
|
1906
2028
|
verb: hasFlag(args, "--hard") ? "hard-prune" : "soft-delete",
|
|
@@ -1956,17 +2078,17 @@ async function handleMigrate(
|
|
|
1956
2078
|
cwd: string,
|
|
1957
2079
|
positionalSkill: string | undefined,
|
|
1958
2080
|
): Promise<StateCommandResult> {
|
|
1959
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1960
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
2081
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "migrate");
|
|
2082
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
|
|
1961
2083
|
if (!mode) {
|
|
1962
2084
|
throw new StateCommandError(
|
|
1963
2085
|
2,
|
|
1964
|
-
"skc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
2086
|
+
"skc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1965
2087
|
);
|
|
1966
2088
|
}
|
|
1967
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
2089
|
+
const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
|
|
1968
2090
|
const forced = hasFlag(args, "--force");
|
|
1969
|
-
const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, filePath, mode, {
|
|
2091
|
+
const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, selectors.skcSessionId, filePath, mode, {
|
|
1970
2092
|
forced,
|
|
1971
2093
|
});
|
|
1972
2094
|
if (mismatchWarning && !forced) {
|
|
@@ -1976,7 +2098,7 @@ async function handleMigrate(
|
|
|
1976
2098
|
cwd,
|
|
1977
2099
|
skill: mode,
|
|
1978
2100
|
statePath: filePath,
|
|
1979
|
-
sessionId: selectors.
|
|
2101
|
+
sessionId: selectors.skcSessionId,
|
|
1980
2102
|
});
|
|
1981
2103
|
return {
|
|
1982
2104
|
status: 0,
|
|
@@ -2018,6 +2140,7 @@ export async function runNativeStateCommand(args: string[], cwd = process.cwd())
|
|
|
2018
2140
|
}
|
|
2019
2141
|
} catch (error) {
|
|
2020
2142
|
if (error instanceof StateCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
|
|
2143
|
+
if (error instanceof SessionResolutionError) return { status: 2, stderr: `${error.message}\n` };
|
|
2021
2144
|
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
2022
2145
|
}
|
|
2023
2146
|
}
|