@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
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as logger from "@sayknow-cli/utils/logger";
|
|
2
|
+
import { activeSnapshotPath, assertNonEmptySkcSessionId, modeStatePath } from "../skc-runtime/session-layout";
|
|
3
|
+
import { resolveSkcSessionForRead, SessionResolutionError } from "../skc-runtime/session-resolution";
|
|
2
4
|
import {
|
|
3
5
|
type ActiveSessionScope,
|
|
4
6
|
readActiveEntries,
|
|
@@ -6,13 +8,12 @@ import {
|
|
|
6
8
|
removeActiveEntry,
|
|
7
9
|
writeActiveEntry,
|
|
8
10
|
} from "../skc-runtime/state-writer";
|
|
11
|
+
import { CANONICAL_SKC_WORKFLOW_SKILLS, type CanonicalSkcWorkflowSkill } from "./canonical-skills";
|
|
9
12
|
import type { WorkflowStateReceipt } from "./workflow-state-contract";
|
|
10
13
|
|
|
11
14
|
export const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
12
15
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
export type CanonicalSkcWorkflowSkill = (typeof CANONICAL_SKC_WORKFLOW_SKILLS)[number];
|
|
16
|
+
export { CANONICAL_SKC_WORKFLOW_SKILLS, type CanonicalSkcWorkflowSkill };
|
|
16
17
|
export type WorkflowHudSeverity = "info" | "warning" | "blocked" | "error" | "success";
|
|
17
18
|
|
|
18
19
|
export interface WorkflowHudChip {
|
|
@@ -60,6 +61,7 @@ export interface SkillActiveEntry {
|
|
|
60
61
|
handoff_to?: string;
|
|
61
62
|
handoff_at?: string;
|
|
62
63
|
active_subskills?: ActiveSubskillEntry[];
|
|
64
|
+
source_state_revision?: number;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export interface SkillActiveState {
|
|
@@ -83,7 +85,7 @@ export interface SkillActiveState {
|
|
|
83
85
|
|
|
84
86
|
export interface SkillActiveStatePaths {
|
|
85
87
|
rootPath: string;
|
|
86
|
-
sessionPath
|
|
88
|
+
sessionPath: string;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
export interface SyncSkillActiveStateOptions {
|
|
@@ -102,6 +104,7 @@ export interface SyncSkillActiveStateOptions {
|
|
|
102
104
|
handoff_to?: string;
|
|
103
105
|
handoff_at?: string;
|
|
104
106
|
active_subskills?: ActiveSubskillEntry[];
|
|
107
|
+
sourceRevision?: number;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
const HUD_TEXT_LIMIT = 80;
|
|
@@ -246,8 +249,12 @@ function unionActiveSubskillEntries(...entrySets: Array<ActiveSubskillEntry[] |
|
|
|
246
249
|
return merged;
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
function
|
|
250
|
-
|
|
252
|
+
function resolveBoundarySessionId(cwd: string, sessionId?: string): Promise<string> {
|
|
253
|
+
const normalizedSessionId = safeString(sessionId).trim();
|
|
254
|
+
if (normalizedSessionId) return Promise.resolve(normalizedSessionId);
|
|
255
|
+
return resolveSkcSessionForRead(cwd, { envSessionId: process.env.SKC_SESSION_ID }).then(
|
|
256
|
+
context => context.skcSessionId,
|
|
257
|
+
);
|
|
251
258
|
}
|
|
252
259
|
|
|
253
260
|
function entryKey(entry: Pick<SkillActiveEntry, "skill" | "session_id">): string {
|
|
@@ -343,14 +350,10 @@ export function normalizeSkillActiveState(raw: unknown): SkillActiveState | null
|
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
export function getSkillActiveStatePaths(cwd: string, sessionId?: string): SkillActiveStatePaths {
|
|
346
|
-
const stateDir = path.join(cwd, ".skc", "state");
|
|
347
|
-
const rootPath = path.join(stateDir, SKILL_ACTIVE_STATE_FILE);
|
|
348
353
|
const normalizedSessionId = safeString(sessionId).trim();
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
sessionPath: path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), SKILL_ACTIVE_STATE_FILE),
|
|
353
|
-
};
|
|
354
|
+
assertNonEmptySkcSessionId(normalizedSessionId, "getSkillActiveStatePaths");
|
|
355
|
+
const sessionPath = activeSnapshotPath(cwd, normalizedSessionId);
|
|
356
|
+
return { rootPath: sessionPath, sessionPath };
|
|
354
357
|
}
|
|
355
358
|
|
|
356
359
|
/**
|
|
@@ -380,7 +383,12 @@ async function readRawActiveStateForHandoff(filePath: string, strict: boolean):
|
|
|
380
383
|
if (!parsed || typeof parsed !== "object") return null;
|
|
381
384
|
return parsed as SkillActiveState;
|
|
382
385
|
} catch (err) {
|
|
383
|
-
if (!strict)
|
|
386
|
+
if (!strict) {
|
|
387
|
+
logger.warn(
|
|
388
|
+
`skc skill-state: invalid skill-active-state at ${filePath}: invalid JSON: ${(err as Error).message}`,
|
|
389
|
+
);
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
384
392
|
throw err;
|
|
385
393
|
}
|
|
386
394
|
}
|
|
@@ -419,14 +427,10 @@ function rawActiveEntries(state: SkillActiveState | null): SkillActiveEntry[] {
|
|
|
419
427
|
|
|
420
428
|
async function readModeStatePhase(
|
|
421
429
|
cwd: string,
|
|
422
|
-
sessionId: string
|
|
430
|
+
sessionId: string,
|
|
423
431
|
skill: CanonicalSkcWorkflowSkill,
|
|
424
432
|
): Promise<string | undefined> {
|
|
425
|
-
const
|
|
426
|
-
const normalizedSessionId = safeString(sessionId).trim();
|
|
427
|
-
const filePath = normalizedSessionId
|
|
428
|
-
? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), `${skill}-state.json`)
|
|
429
|
-
: path.join(stateDir, `${skill}-state.json`);
|
|
433
|
+
const filePath = modeStatePath(cwd, sessionId, skill);
|
|
430
434
|
try {
|
|
431
435
|
const parsed = JSON.parse(await Bun.file(filePath).text());
|
|
432
436
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
@@ -473,15 +477,6 @@ function withCanonicalRalplanPhase(entry: SkillActiveEntry, canonicalPhase: stri
|
|
|
473
477
|
};
|
|
474
478
|
}
|
|
475
479
|
|
|
476
|
-
function filterRootEntriesForSession(entries: SkillActiveEntry[], sessionId?: string): SkillActiveEntry[] {
|
|
477
|
-
const normalizedSessionId = safeString(sessionId).trim();
|
|
478
|
-
if (!normalizedSessionId) return entries;
|
|
479
|
-
return entries.filter(entry => {
|
|
480
|
-
const entrySessionId = safeString(entry.session_id).trim();
|
|
481
|
-
return entrySessionId.length === 0 || entrySessionId === normalizedSessionId;
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
480
|
function entryRecency(entry: SkillActiveEntry): number {
|
|
486
481
|
const stamp = entry.handoff_at || entry.updated_at || entry.activated_at;
|
|
487
482
|
const ms = stamp ? Date.parse(stamp) : Number.NaN;
|
|
@@ -559,94 +554,94 @@ function dedupeVisibleBySkill(entries: SkillActiveEntry[], sessionId?: string):
|
|
|
559
554
|
|
|
560
555
|
/**
|
|
561
556
|
* The planning pipeline advances one stage at a time: `deep-interview →
|
|
562
|
-
* ralplan → ultragoal`.
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
* `active:true` would render both stages and keep showing a workflow that has
|
|
567
|
-
* already handed control forward. Keep only the most recently updated pipeline
|
|
568
|
-
* stage so the HUD reflects the single current workflow. `team` is intentionally
|
|
569
|
-
* excluded — it runs alongside ultragoal — and every non-pipeline skill is left
|
|
570
|
-
* untouched.
|
|
571
|
-
*
|
|
572
|
-
* This is a HUD-display policy only. It is applied by the skill HUD renderer and
|
|
573
|
-
* deliberately NOT folded into `readVisibleSkillActiveState`, whose callers (the
|
|
574
|
-
* deep-interview mutation guard and handoff caller inference) must keep seeing
|
|
575
|
-
* every genuinely-active skill rather than the single most-recent pipeline stage.
|
|
557
|
+
* ralplan → ultragoal`. Activating a downstream stage supersedes upstream
|
|
558
|
+
* stages so stale rows cannot keep owning the HUD, gate, or primary active
|
|
559
|
+
* snapshot. `team` is intentionally excluded — it runs alongside ultragoal —
|
|
560
|
+
* and every non-pipeline skill is left untouched.
|
|
576
561
|
*/
|
|
577
562
|
const PLANNING_PIPELINE_SKILLS = new Set<string>(["deep-interview", "ralplan", "ultragoal"]);
|
|
563
|
+
const PLANNING_PIPELINE_RANK = new Map<string, number>([
|
|
564
|
+
["deep-interview", 0],
|
|
565
|
+
["ralplan", 1],
|
|
566
|
+
["ultragoal", 2],
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
function planningPipelineRank(skill: string): number | undefined {
|
|
570
|
+
return PLANNING_PIPELINE_RANK.get(skill);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function comparePipelineEntry(a: SkillActiveEntry, b: SkillActiveEntry): number {
|
|
574
|
+
const aRank = planningPipelineRank(a.skill);
|
|
575
|
+
const bRank = planningPipelineRank(b.skill);
|
|
576
|
+
if (aRank !== undefined || bRank !== undefined) return (bRank ?? -1) - (aRank ?? -1);
|
|
577
|
+
const aRecency = entryRecency(a);
|
|
578
|
+
const bRecency = entryRecency(b);
|
|
579
|
+
if (Number.isFinite(aRecency) || Number.isFinite(bRecency)) return (bRecency || 0) - (aRecency || 0);
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function upstreamPlanningPipelineSkills(skill: string): string[] {
|
|
584
|
+
const rank = planningPipelineRank(skill);
|
|
585
|
+
if (rank === undefined) return [];
|
|
586
|
+
return [...PLANNING_PIPELINE_RANK.entries()]
|
|
587
|
+
.filter(([, candidateRank]) => candidateRank < rank)
|
|
588
|
+
.map(([candidate]) => candidate);
|
|
589
|
+
}
|
|
578
590
|
|
|
579
591
|
export function collapsePlanningPipeline(entries: readonly SkillActiveEntry[]): SkillActiveEntry[] {
|
|
580
592
|
const pipeline = entries.filter(entry => PLANNING_PIPELINE_SKILLS.has(entry.skill));
|
|
581
593
|
if (pipeline.length <= 1) return [...entries];
|
|
582
|
-
|
|
583
|
-
let currentRecency = entryRecency(current);
|
|
584
|
-
for (const entry of pipeline) {
|
|
585
|
-
const recency = entryRecency(entry);
|
|
586
|
-
// Prefer a strictly-newer valid timestamp; a valid timestamp also beats a
|
|
587
|
-
// missing/unparseable one. Ties (or all-invalid) keep the first stage
|
|
588
|
-
// deterministically rather than letting an unknown-recency row win.
|
|
589
|
-
const better = Number.isFinite(recency) && (!Number.isFinite(currentRecency) || recency > currentRecency);
|
|
590
|
-
if (better) {
|
|
591
|
-
current = entry;
|
|
592
|
-
currentRecency = recency;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
594
|
+
const current = pipeline.toSorted(comparePipelineEntry)[0];
|
|
595
595
|
return entries.filter(entry => !PLANNING_PIPELINE_SKILLS.has(entry.skill) || entry === current);
|
|
596
596
|
}
|
|
597
597
|
|
|
598
598
|
async function mergeVisibleEntries(
|
|
599
599
|
cwd: string,
|
|
600
600
|
sessionState: SkillActiveState | null,
|
|
601
|
-
|
|
602
|
-
sessionId?: string,
|
|
601
|
+
sessionId: string,
|
|
603
602
|
): Promise<SkillActiveEntry[]> {
|
|
604
603
|
// Use the raw (active + inactive) rows so a handoff demotion stays visible
|
|
605
604
|
// long enough to supersede a stale same-skill row before the active filter.
|
|
606
605
|
// Per-skill files in active/<skill>.json are authoritative and are merged
|
|
607
606
|
// after the derived snapshot cache, so a stale skill-active-state.json row
|
|
608
607
|
// cannot override the latest entry file.
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
sessionId,
|
|
612
|
-
);
|
|
613
|
-
const merged = new Map(rootEntries.map(entry => [entryKey(entry), entry]));
|
|
614
|
-
const sessionEntries = sessionId
|
|
615
|
-
? [...rawActiveEntries(sessionState), ...(await readActiveEntries(cwd, { sessionId }))]
|
|
616
|
-
: rawActiveEntries(sessionState);
|
|
617
|
-
for (const entry of sessionEntries) {
|
|
618
|
-
merged.set(entryKey(entry), entry);
|
|
619
|
-
}
|
|
608
|
+
const entries = [...rawActiveEntries(sessionState), ...(await readActiveEntries(cwd, { sessionId }))];
|
|
609
|
+
const merged = new Map(entries.map(entry => [entryKey(entry), entry]));
|
|
620
610
|
const canonicalRalplanPhase = await readModeStatePhase(cwd, sessionId, "ralplan");
|
|
621
|
-
|
|
611
|
+
const visibleEntries = dedupeVisibleBySkill([...merged.values()], sessionId)
|
|
622
612
|
.filter(entry => entry.active !== false)
|
|
623
613
|
.map(entry => withCanonicalRalplanPhase(entry, canonicalRalplanPhase));
|
|
614
|
+
return collapsePlanningPipeline(visibleEntries).toSorted(comparePipelineEntry);
|
|
624
615
|
}
|
|
625
616
|
|
|
626
617
|
export async function readVisibleSkillActiveState(cwd: string, sessionId?: string): Promise<SkillActiveState | null> {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
618
|
+
let resolvedSessionId: string;
|
|
619
|
+
try {
|
|
620
|
+
resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error instanceof SessionResolutionError && error.code === "no_session") return null;
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
const { sessionPath } = getSkillActiveStatePaths(cwd, resolvedSessionId);
|
|
626
|
+
const sessionState = await readRawActiveStateForHandoff(sessionPath, false);
|
|
627
|
+
const activeSkills = await mergeVisibleEntries(cwd, sessionState, resolvedSessionId);
|
|
633
628
|
if (activeSkills.length === 0) return null;
|
|
634
629
|
const primary = activeSkills[0];
|
|
635
630
|
return {
|
|
636
|
-
...(rootState ?? {}),
|
|
637
631
|
...(sessionState ?? {}),
|
|
638
632
|
version: 1,
|
|
639
633
|
active: true,
|
|
640
|
-
skill: primary?.skill ?? "",
|
|
641
|
-
phase: primary?.phase ?? "",
|
|
642
|
-
session_id:
|
|
634
|
+
skill: sessionState?.skill ?? primary?.skill ?? "",
|
|
635
|
+
phase: sessionState?.phase ?? primary?.phase ?? "",
|
|
636
|
+
session_id: resolvedSessionId,
|
|
643
637
|
active_skills: activeSkills,
|
|
644
638
|
active_subskills: activeSkills.flatMap(entry => entry.active_subskills ?? []),
|
|
645
639
|
};
|
|
646
640
|
}
|
|
647
641
|
|
|
648
|
-
function activeStateWriterAudit(verb: string) {
|
|
649
|
-
|
|
642
|
+
function activeStateWriterAudit(verb: string, sessionScope?: ActiveSessionScope | string) {
|
|
643
|
+
const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
|
|
644
|
+
return { category: "state" as const, verb, owner: "skc-runtime" as const, ...(sessionId ? { sessionId } : {}) };
|
|
650
645
|
}
|
|
651
646
|
|
|
652
647
|
async function persistActiveEntry(
|
|
@@ -657,12 +652,13 @@ async function persistActiveEntry(
|
|
|
657
652
|
if (entry.active === false) {
|
|
658
653
|
await removeActiveEntry(cwd, sessionScope, entry.skill, {
|
|
659
654
|
cwd,
|
|
660
|
-
audit: activeStateWriterAudit("remove-active-entry"),
|
|
655
|
+
audit: activeStateWriterAudit("remove-active-entry", sessionScope),
|
|
656
|
+
sourceRevision: entry.source_state_revision,
|
|
661
657
|
});
|
|
662
658
|
} else {
|
|
663
659
|
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
664
660
|
cwd,
|
|
665
|
-
audit: activeStateWriterAudit("write-active-entry"),
|
|
661
|
+
audit: activeStateWriterAudit("write-active-entry", sessionScope),
|
|
666
662
|
});
|
|
667
663
|
}
|
|
668
664
|
}
|
|
@@ -674,12 +670,29 @@ async function writeHandoffEntry(
|
|
|
674
670
|
): Promise<void> {
|
|
675
671
|
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
676
672
|
cwd,
|
|
677
|
-
audit: activeStateWriterAudit("write-active-entry"),
|
|
673
|
+
audit: activeStateWriterAudit("write-active-entry", sessionScope),
|
|
678
674
|
});
|
|
679
675
|
}
|
|
680
676
|
|
|
681
677
|
async function rebuildActiveState(cwd: string, sessionScope?: ActiveSessionScope): Promise<void> {
|
|
682
|
-
await rebuildActiveSnapshot(cwd, sessionScope, {
|
|
678
|
+
await rebuildActiveSnapshot(cwd, sessionScope, {
|
|
679
|
+
cwd,
|
|
680
|
+
audit: activeStateWriterAudit("rebuild-active-snapshot", sessionScope),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function removeSupersededPlanningPipelineEntries(
|
|
685
|
+
cwd: string,
|
|
686
|
+
sessionScope: ActiveSessionScope | undefined,
|
|
687
|
+
entry: SkillActiveEntry,
|
|
688
|
+
): Promise<void> {
|
|
689
|
+
if (entry.active === false) return;
|
|
690
|
+
for (const skill of upstreamPlanningPipelineSkills(entry.skill)) {
|
|
691
|
+
await removeActiveEntry(cwd, sessionScope, skill, {
|
|
692
|
+
cwd,
|
|
693
|
+
audit: activeStateWriterAudit("remove-superseded-pipeline-entry", sessionScope),
|
|
694
|
+
});
|
|
695
|
+
}
|
|
683
696
|
}
|
|
684
697
|
|
|
685
698
|
async function activeSubskillsForExistingEntry(
|
|
@@ -687,18 +700,17 @@ async function activeSubskillsForExistingEntry(
|
|
|
687
700
|
sessionId: string | undefined,
|
|
688
701
|
skill: string,
|
|
689
702
|
): Promise<ActiveSubskillEntry[] | undefined> {
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
]);
|
|
695
|
-
const existing = (await mergeVisibleEntries(cwd, sessionState, rootState, sessionId)).find(
|
|
703
|
+
const resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
|
|
704
|
+
const { sessionPath } = getSkillActiveStatePaths(cwd, resolvedSessionId);
|
|
705
|
+
const sessionState = await readRawActiveStateForHandoff(sessionPath, false);
|
|
706
|
+
const existing = (await mergeVisibleEntries(cwd, sessionState, resolvedSessionId)).find(
|
|
696
707
|
entry => entry.skill === skill,
|
|
697
708
|
);
|
|
698
709
|
return existing?.active_subskills;
|
|
699
710
|
}
|
|
700
711
|
|
|
701
712
|
export async function syncSkillActiveState(options: SyncSkillActiveStateOptions): Promise<void> {
|
|
713
|
+
if (!options.sessionId) return;
|
|
702
714
|
const preservedActiveSubskills =
|
|
703
715
|
options.active_subskills === undefined
|
|
704
716
|
? await activeSubskillsForExistingEntry(options.cwd, options.sessionId, options.skill)
|
|
@@ -724,12 +736,10 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
724
736
|
: preservedActiveSubskills
|
|
725
737
|
? { active_subskills: preservedActiveSubskills }
|
|
726
738
|
: {}),
|
|
739
|
+
...(typeof options.sourceRevision === "number" ? { source_state_revision: options.sourceRevision } : {}),
|
|
727
740
|
};
|
|
728
|
-
await persistActiveEntry(options.cwd, undefined, entry);
|
|
729
|
-
await rebuildActiveState(options.cwd);
|
|
730
|
-
|
|
731
|
-
if (!options.sessionId) return;
|
|
732
741
|
const sessionScope = { sessionId: options.sessionId };
|
|
742
|
+
await removeSupersededPlanningPipelineEntries(options.cwd, sessionScope, entry);
|
|
733
743
|
await persistActiveEntry(options.cwd, sessionScope, entry);
|
|
734
744
|
await rebuildActiveState(options.cwd, sessionScope);
|
|
735
745
|
}
|
|
@@ -745,36 +755,23 @@ export interface ApplyHandoffOptions {
|
|
|
745
755
|
}
|
|
746
756
|
|
|
747
757
|
/**
|
|
748
|
-
* Atomically apply a workflow-skill handoff to
|
|
749
|
-
* root `skill-active-state.json` files in a single write per file.
|
|
750
|
-
*
|
|
751
|
-
* Write order: **session first, root last**. The session file is the
|
|
752
|
-
* source of truth for HUD; the root aggregate must never lead the session
|
|
753
|
-
* during a handoff window. Each file is rewritten once with caller demoted
|
|
754
|
-
* to `active:false` (preserving `handoff_to`/`handoff_at` lineage) and
|
|
755
|
-
* callee promoted to `active:true` (with `handoff_from`/`handoff_at`).
|
|
758
|
+
* Atomically apply a workflow-skill handoff to the session-scoped active state.
|
|
756
759
|
*/
|
|
757
760
|
export async function applyHandoffToActiveState(options: ApplyHandoffOptions): Promise<void> {
|
|
758
761
|
const nowIso = options.nowIso ?? new Date().toISOString();
|
|
759
762
|
const callerEntry = buildSyncEntry(options.caller, nowIso);
|
|
760
763
|
const calleeEntry = buildSyncEntry(options.callee, nowIso);
|
|
761
764
|
const sessionId = options.callee.sessionId ?? options.caller.sessionId;
|
|
762
|
-
|
|
765
|
+
assertNonEmptySkcSessionId(sessionId, "applyHandoffToActiveState");
|
|
766
|
+
const { sessionPath } = getSkillActiveStatePaths(options.cwd, sessionId);
|
|
763
767
|
const readState = (filePath: string) => readRawActiveStateForHandoff(filePath, options.strict === true);
|
|
764
|
-
await
|
|
765
|
-
|
|
766
|
-
// A skill can hold more than one visible row in this session's scope — e.g.
|
|
767
|
-
// it was seeded without a session id (rendered globally) and is now handed
|
|
768
|
-
// off under a concrete session id. Supersede every same-session-scope row of
|
|
769
|
-
// the caller and callee skills, not just the exact `skill::session_id` key,
|
|
770
|
-
// so a stale `active:true` row cannot survive the demotion and keep showing
|
|
771
|
-
// in the HUD. Rows owned by other sessions are left untouched.
|
|
768
|
+
await readState(sessionPath);
|
|
769
|
+
|
|
772
770
|
const handoffSession = safeString(sessionId).trim();
|
|
773
771
|
const reassignedSkills = new Set([callerEntry.skill, calleeEntry.skill]);
|
|
774
772
|
const supersedesVisible = (entry: SkillActiveEntry): boolean => {
|
|
775
773
|
if (!reassignedSkills.has(entry.skill)) return false;
|
|
776
|
-
|
|
777
|
-
return entrySession.length === 0 || entrySession === handoffSession;
|
|
774
|
+
return safeString(entry.session_id).trim() === handoffSession;
|
|
778
775
|
};
|
|
779
776
|
const applyEntries = (entries: SkillActiveEntry[]): SkillActiveEntry[] => {
|
|
780
777
|
const callerKey = entryKey(callerEntry);
|
|
@@ -782,9 +779,6 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
782
779
|
entries.find(e => entryKey(e) === callerKey) ??
|
|
783
780
|
entries.find(e => e.skill === callerEntry.skill && supersedesVisible(e) && Boolean(e.handoff_from));
|
|
784
781
|
const kept = entries.filter(e => !supersedesVisible(e));
|
|
785
|
-
// Merge prior lineage into the demoted caller so multi-step handoff
|
|
786
|
-
// chains preserve `handoff_from` from the previous transition while
|
|
787
|
-
// the new `handoff_to`/`handoff_at` describe this one.
|
|
788
782
|
const mergedCaller: SkillActiveEntry = priorCaller
|
|
789
783
|
? {
|
|
790
784
|
...callerEntry,
|
|
@@ -799,10 +793,7 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
799
793
|
activeSubskills.length > 0 ? { ...calleeEntry, active_subskills: activeSubskills } : calleeEntry;
|
|
800
794
|
return [...kept, mergedCaller, mergedCallee];
|
|
801
795
|
};
|
|
802
|
-
const writeEntries = async (
|
|
803
|
-
sessionScope: ActiveSessionScope | undefined,
|
|
804
|
-
prior: SkillActiveState | null,
|
|
805
|
-
): Promise<void> => {
|
|
796
|
+
const writeEntries = async (sessionScope: ActiveSessionScope, prior: SkillActiveState | null): Promise<void> => {
|
|
806
797
|
const nextEntries = applyEntries(rawActiveEntries(prior));
|
|
807
798
|
for (const entry of nextEntries) {
|
|
808
799
|
await writeHandoffEntry(options.cwd, sessionScope, entry);
|
|
@@ -810,12 +801,8 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
810
801
|
await rebuildActiveState(options.cwd, sessionScope);
|
|
811
802
|
};
|
|
812
803
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
await writeEntries({ sessionId }, prior);
|
|
816
|
-
}
|
|
817
|
-
const priorRoot = await readState(rootPath);
|
|
818
|
-
await writeEntries(undefined, priorRoot);
|
|
804
|
+
const prior = await readState(sessionPath);
|
|
805
|
+
await writeEntries({ sessionId }, prior);
|
|
819
806
|
}
|
|
820
807
|
|
|
821
808
|
function buildSyncEntry(options: SyncSkillActiveStateOptions, nowIso: string): SkillActiveEntry {
|