@gajae-code/coding-agent 0.6.3 → 0.6.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/CHANGELOG.md +50 -0
- package/README.md +73 -1
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- 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/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -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/src/cli/migrate-cli.ts +106 -0
- package/src/cli/setup-cli.ts +14 -1
- 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/settings-schema.ts +17 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +51 -47
- package/src/defaults/gjc/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/gjc-plugins/injection.ts +23 -4
- package/src/extensibility/gjc-plugins/state.ts +16 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
- package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
- package/src/gjc-runtime/goal-mode-request.ts +26 -11
- package/src/gjc-runtime/launch-tmux.ts +68 -15
- package/src/gjc-runtime/ralplan-runtime.ts +79 -50
- package/src/gjc-runtime/session-layout.ts +180 -0
- package/src/gjc-runtime/session-resolution.ts +217 -0
- package/src/gjc-runtime/state-graph.ts +1 -2
- package/src/gjc-runtime/state-migrations.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +230 -121
- package/src/gjc-runtime/state-schema.ts +2 -0
- package/src/gjc-runtime/state-writer.ts +289 -41
- package/src/gjc-runtime/team-runtime.ts +43 -19
- package/src/gjc-runtime/tmux-sessions.ts +43 -2
- package/src/gjc-runtime/ultragoal-guard.ts +45 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.ts +1 -2
- 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 +9 -6
- 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 +42 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +22 -1
- 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 +15 -21
- 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/skill-state/active-state.ts +72 -108
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
- 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/computer.ts +58 -4
- 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
|
@@ -149,6 +149,52 @@ export async function updateMCPServer(filePath: string, name: string, config: MC
|
|
|
149
149
|
await writeMCPConfigFile(filePath, updated);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Result of an {@link upsertMCPServer} call.
|
|
154
|
+
* - `added`: server did not exist and was written.
|
|
155
|
+
* - `updated`: server existed and was overwritten because `force` was set.
|
|
156
|
+
* - `skipped`: server existed and `force` was not set, so nothing was written.
|
|
157
|
+
*/
|
|
158
|
+
export type UpsertMCPServerResult =
|
|
159
|
+
| { status: "added" }
|
|
160
|
+
| { status: "updated" }
|
|
161
|
+
| { status: "skipped"; reason: "exists" };
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Add an MCP server, or overwrite an existing one only when `force` is set.
|
|
165
|
+
*
|
|
166
|
+
* Collision-aware wrapper over {@link addMCPServer} / {@link updateMCPServer} used by
|
|
167
|
+
* `gjc migrate`. Never connects to the server. Reuses the underlying writers so the
|
|
168
|
+
* rest of the config file (including `disabledServers`) is preserved on update.
|
|
169
|
+
*
|
|
170
|
+
* @throws Error if the server name or config is invalid (validated before any write).
|
|
171
|
+
*/
|
|
172
|
+
export async function upsertMCPServer(
|
|
173
|
+
filePath: string,
|
|
174
|
+
name: string,
|
|
175
|
+
config: MCPServerConfig,
|
|
176
|
+
options: { force?: boolean } = {},
|
|
177
|
+
): Promise<UpsertMCPServerResult> {
|
|
178
|
+
// Validate name up front so an invalid name fails regardless of collision state.
|
|
179
|
+
const nameError = validateServerName(name);
|
|
180
|
+
if (nameError) {
|
|
181
|
+
throw new Error(nameError);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const existing = await getMCPServer(filePath, name);
|
|
185
|
+
if (existing) {
|
|
186
|
+
if (!options.force) {
|
|
187
|
+
return { status: "skipped", reason: "exists" };
|
|
188
|
+
}
|
|
189
|
+
// updateMCPServer preserves the rest of MCPConfigFile, incl. disabledServers.
|
|
190
|
+
await updateMCPServer(filePath, name, config);
|
|
191
|
+
return { status: "updated" };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await addMCPServer(filePath, name, config);
|
|
195
|
+
return { status: "added" };
|
|
196
|
+
}
|
|
197
|
+
|
|
152
198
|
/**
|
|
153
199
|
* Remove an MCP server from a config file.
|
|
154
200
|
*
|
|
@@ -183,6 +183,11 @@ import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
|
183
183
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
184
184
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
185
185
|
import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
|
|
186
|
+
import {
|
|
187
|
+
assertNonEmptyGjcSessionId,
|
|
188
|
+
modeStatePath as sessionModeStatePath,
|
|
189
|
+
sessionStateDir,
|
|
190
|
+
} from "../gjc-runtime/session-layout";
|
|
186
191
|
import { persistCoordinatorRuntimeStateFromEvent } from "../gjc-runtime/session-state-sidecar";
|
|
187
192
|
import { writeArtifact } from "../gjc-runtime/state-writer";
|
|
188
193
|
import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
|
|
@@ -312,13 +317,6 @@ export type AgentSessionEvent =
|
|
|
312
317
|
| { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
|
|
313
318
|
| { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
|
|
314
319
|
|
|
315
|
-
/**
|
|
316
|
-
* Safe path component pattern used to validate session-id segments before
|
|
317
|
-
* joining them into `.gjc/state` paths. Mirrors the regex used by the
|
|
318
|
-
* `gjc state` runtime selector resolver.
|
|
319
|
-
*/
|
|
320
|
-
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
321
|
-
|
|
322
320
|
function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
323
321
|
const relative = path.relative(path.join(path.resolve(cwd), ".gjc"), path.resolve(targetPath));
|
|
324
322
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
@@ -1370,21 +1368,17 @@ export class AgentSession {
|
|
|
1370
1368
|
getActiveSkillPhase(): string | undefined {
|
|
1371
1369
|
const active = this.#activeSkillState;
|
|
1372
1370
|
if (!active) return undefined;
|
|
1373
|
-
// Path safety: refuse to read mode-state files when the skill or
|
|
1374
|
-
// session-id are not safe path components. The `skill` tool
|
|
1375
|
-
// interprets undefined as a non-terminal phase, so chaining is
|
|
1376
|
-
// refused — there is no risk of bypassing the guard via a custom
|
|
1377
|
-
// skill name with `..` or a session-id with separators.
|
|
1378
1371
|
if (!isCanonicalGjcWorkflowSkill(active.skill)) return undefined;
|
|
1379
|
-
|
|
1380
|
-
return undefined;
|
|
1381
|
-
}
|
|
1372
|
+
const sessionId = active.sessionId ?? this.sessionManager.getSessionId();
|
|
1382
1373
|
try {
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const filePath = path.join(
|
|
1374
|
+
assertNonEmptyGjcSessionId(sessionId, "AgentSession.getActiveSkillPhase");
|
|
1375
|
+
// Keep the session-state-dir construction explicit here so the chain guard
|
|
1376
|
+
// refuses to fall back to a legacy root `.gjc/state` read.
|
|
1377
|
+
const stateDir = sessionStateDir(this.sessionManager.getCwd(), sessionId);
|
|
1378
|
+
const filePath = path.join(
|
|
1379
|
+
stateDir,
|
|
1380
|
+
path.basename(sessionModeStatePath(this.sessionManager.getCwd(), sessionId, active.skill)),
|
|
1381
|
+
);
|
|
1388
1382
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1389
1383
|
const parsed = JSON.parse(raw) as { current_phase?: unknown };
|
|
1390
1384
|
return typeof parsed.current_phase === "string" ? parsed.current_phase : undefined;
|
|
@@ -3763,7 +3757,7 @@ export class AgentSession {
|
|
|
3763
3757
|
* prompts or tool execution can run.
|
|
3764
3758
|
*/
|
|
3765
3759
|
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3766
|
-
if (!["edit", "write", "ast_edit"
|
|
3760
|
+
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
3767
3761
|
return new Proxy(tool, {
|
|
3768
3762
|
get: (target, prop) => {
|
|
3769
3763
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -1129,6 +1129,23 @@ function formatTimeAgo(date: Date): string {
|
|
|
1129
1129
|
return date.toLocaleDateString();
|
|
1130
1130
|
}
|
|
1131
1131
|
|
|
1132
|
+
async function movePathAcrossDevicesSafe(source: string, destination: string): Promise<void> {
|
|
1133
|
+
try {
|
|
1134
|
+
await fs.promises.rename(source, destination);
|
|
1135
|
+
return;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (!hasFsCode(error, "EXDEV")) throw error;
|
|
1138
|
+
}
|
|
1139
|
+
const stat = await fs.promises.stat(source);
|
|
1140
|
+
if (stat.isDirectory()) {
|
|
1141
|
+
await fs.promises.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
1142
|
+
await fs.promises.rm(source, { recursive: true, force: false });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
|
|
1146
|
+
await fs.promises.unlink(source);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1132
1149
|
const MAX_PERSIST_CHARS = 500_000;
|
|
1133
1150
|
const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
|
|
1134
1151
|
/** Minimum base64 length to externalize to blob store (skip tiny inline images) */
|
|
@@ -2498,14 +2515,14 @@ export class SessionManager {
|
|
|
2498
2515
|
try {
|
|
2499
2516
|
// Guard: session file may not exist yet (no assistant messages persisted)
|
|
2500
2517
|
if (hadSessionFile) {
|
|
2501
|
-
await
|
|
2518
|
+
await movePathAcrossDevicesSafe(oldSessionFile, newSessionFile);
|
|
2502
2519
|
movedSessionFile = true;
|
|
2503
2520
|
}
|
|
2504
2521
|
|
|
2505
2522
|
try {
|
|
2506
2523
|
const stat = await fs.promises.stat(oldArtifactDir);
|
|
2507
2524
|
if (stat.isDirectory()) {
|
|
2508
|
-
await
|
|
2525
|
+
await movePathAcrossDevicesSafe(oldArtifactDir, newArtifactDir);
|
|
2509
2526
|
movedArtifactDir = true;
|
|
2510
2527
|
}
|
|
2511
2528
|
} catch (err) {
|
|
@@ -29,6 +29,14 @@ The Hermes bridge does not choose a model/provider. Generated setup configures `
|
|
|
29
29
|
|
|
30
30
|
Provider-specific commands are examples only, never product defaults.
|
|
31
31
|
|
|
32
|
+
## Visible routed-session fallback
|
|
33
|
+
|
|
34
|
+
If a Hermes/OpenClaw/Clawhip-style operator needs a human-visible, channel-routed GJC pane instead of a pure Coordinator MCP session, use the visible session pattern in [`docs/gjc-session-clawhip-routing.md`](../../../../../../docs/gjc-session-clawhip-routing.md).
|
|
35
|
+
|
|
36
|
+
Use that pattern only when the router must watch tmux output, send stale-session alerts, or inject follow-up prompts into the same visible pane. The short version is: prepare a dedicated worktree, register a stable tmux session through the host router, start interactive `gjc`, wait for TUI readiness, inject the task prompt separately, and verify actual tool/work activity before reporting acceptance.
|
|
37
|
+
|
|
38
|
+
Do not put private channel ids, mention targets, socket names, tokens, or local routing policy into portable setup output. Keep those in the host/operator deployment.
|
|
39
|
+
|
|
32
40
|
## Safety
|
|
33
41
|
|
|
34
42
|
- Mutating tools require bridge startup mutation classes and per-call consent.
|
|
@@ -404,7 +404,7 @@ async function installConfig(spec: CoordinatorSetupSpec, force: boolean): Promis
|
|
|
404
404
|
|
|
405
405
|
async function runSmoke(spec: CoordinatorSetupSpec): Promise<HermesSetupResult["smoke"]> {
|
|
406
406
|
const requiredTools = [...COORDINATOR_MCP_TOOL_NAMES];
|
|
407
|
-
const server = createCoordinatorMcpServer({ env:
|
|
407
|
+
const server = createCoordinatorMcpServer({ env: renderHermesServerBlock(spec).env as NodeJS.ProcessEnv });
|
|
408
408
|
const listed = await server.handleJsonRpc({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} });
|
|
409
409
|
const listedResult = isRecord(listed.result) ? listed.result : {};
|
|
410
410
|
const tools = Array.isArray(listedResult.tools) ? listedResult.tools : [];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as logger from "@gajae-code/utils/logger";
|
|
2
|
+
import { activeSnapshotPath, assertNonEmptyGjcSessionId, modeStatePath } from "../gjc-runtime/session-layout";
|
|
3
|
+
import { resolveGjcSessionForRead, SessionResolutionError } from "../gjc-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 "../gjc-runtime/state-writer";
|
|
11
|
+
import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill } 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 CanonicalGjcWorkflowSkill = (typeof CANONICAL_GJC_WORKFLOW_SKILLS)[number];
|
|
16
|
+
export { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill };
|
|
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 resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID }).then(
|
|
256
|
+
context => context.gjcSessionId,
|
|
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, ".gjc", "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
|
+
assertNonEmptyGjcSessionId(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
|
+
`gjc 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: CanonicalGjcWorkflowSkill,
|
|
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;
|
|
@@ -603,57 +598,50 @@ export function collapsePlanningPipeline(entries: readonly SkillActiveEntry[]):
|
|
|
603
598
|
async function mergeVisibleEntries(
|
|
604
599
|
cwd: string,
|
|
605
600
|
sessionState: SkillActiveState | null,
|
|
606
|
-
|
|
607
|
-
sessionId?: string,
|
|
601
|
+
sessionId: string,
|
|
608
602
|
): Promise<SkillActiveEntry[]> {
|
|
609
603
|
// Use the raw (active + inactive) rows so a handoff demotion stays visible
|
|
610
604
|
// long enough to supersede a stale same-skill row before the active filter.
|
|
611
605
|
// Per-skill files in active/<skill>.json are authoritative and are merged
|
|
612
606
|
// after the derived snapshot cache, so a stale skill-active-state.json row
|
|
613
607
|
// cannot override the latest entry file.
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
sessionId,
|
|
617
|
-
);
|
|
618
|
-
const merged = new Map(rootEntries.map(entry => [entryKey(entry), entry]));
|
|
619
|
-
const sessionEntries = sessionId
|
|
620
|
-
? [...rawActiveEntries(sessionState), ...(await readActiveEntries(cwd, { sessionId }))]
|
|
621
|
-
: rawActiveEntries(sessionState);
|
|
622
|
-
for (const entry of sessionEntries) {
|
|
623
|
-
merged.set(entryKey(entry), entry);
|
|
624
|
-
}
|
|
608
|
+
const entries = [...rawActiveEntries(sessionState), ...(await readActiveEntries(cwd, { sessionId }))];
|
|
609
|
+
const merged = new Map(entries.map(entry => [entryKey(entry), entry]));
|
|
625
610
|
const canonicalRalplanPhase = await readModeStatePhase(cwd, sessionId, "ralplan");
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
);
|
|
611
|
+
const visibleEntries = dedupeVisibleBySkill([...merged.values()], sessionId)
|
|
612
|
+
.filter(entry => entry.active !== false)
|
|
613
|
+
.map(entry => withCanonicalRalplanPhase(entry, canonicalRalplanPhase));
|
|
614
|
+
return collapsePlanningPipeline(visibleEntries).toSorted(comparePipelineEntry);
|
|
631
615
|
}
|
|
632
616
|
|
|
633
617
|
export async function readVisibleSkillActiveState(cwd: string, sessionId?: string): Promise<SkillActiveState | null> {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
640
628
|
if (activeSkills.length === 0) return null;
|
|
641
629
|
const primary = activeSkills[0];
|
|
642
630
|
return {
|
|
643
|
-
...(rootState ?? {}),
|
|
644
631
|
...(sessionState ?? {}),
|
|
645
632
|
version: 1,
|
|
646
633
|
active: true,
|
|
647
|
-
skill: primary?.skill ?? "",
|
|
648
|
-
phase: primary?.phase ?? "",
|
|
649
|
-
session_id:
|
|
634
|
+
skill: sessionState?.skill ?? primary?.skill ?? "",
|
|
635
|
+
phase: sessionState?.phase ?? primary?.phase ?? "",
|
|
636
|
+
session_id: resolvedSessionId,
|
|
650
637
|
active_skills: activeSkills,
|
|
651
638
|
active_subskills: activeSkills.flatMap(entry => entry.active_subskills ?? []),
|
|
652
639
|
};
|
|
653
640
|
}
|
|
654
641
|
|
|
655
|
-
function activeStateWriterAudit(verb: string) {
|
|
656
|
-
|
|
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: "gjc-runtime" as const, ...(sessionId ? { sessionId } : {}) };
|
|
657
645
|
}
|
|
658
646
|
|
|
659
647
|
async function persistActiveEntry(
|
|
@@ -664,12 +652,13 @@ async function persistActiveEntry(
|
|
|
664
652
|
if (entry.active === false) {
|
|
665
653
|
await removeActiveEntry(cwd, sessionScope, entry.skill, {
|
|
666
654
|
cwd,
|
|
667
|
-
audit: activeStateWriterAudit("remove-active-entry"),
|
|
655
|
+
audit: activeStateWriterAudit("remove-active-entry", sessionScope),
|
|
656
|
+
sourceRevision: entry.source_state_revision,
|
|
668
657
|
});
|
|
669
658
|
} else {
|
|
670
659
|
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
671
660
|
cwd,
|
|
672
|
-
audit: activeStateWriterAudit("write-active-entry"),
|
|
661
|
+
audit: activeStateWriterAudit("write-active-entry", sessionScope),
|
|
673
662
|
});
|
|
674
663
|
}
|
|
675
664
|
}
|
|
@@ -681,12 +670,15 @@ async function writeHandoffEntry(
|
|
|
681
670
|
): Promise<void> {
|
|
682
671
|
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
683
672
|
cwd,
|
|
684
|
-
audit: activeStateWriterAudit("write-active-entry"),
|
|
673
|
+
audit: activeStateWriterAudit("write-active-entry", sessionScope),
|
|
685
674
|
});
|
|
686
675
|
}
|
|
687
676
|
|
|
688
677
|
async function rebuildActiveState(cwd: string, sessionScope?: ActiveSessionScope): Promise<void> {
|
|
689
|
-
await rebuildActiveSnapshot(cwd, sessionScope, {
|
|
678
|
+
await rebuildActiveSnapshot(cwd, sessionScope, {
|
|
679
|
+
cwd,
|
|
680
|
+
audit: activeStateWriterAudit("rebuild-active-snapshot", sessionScope),
|
|
681
|
+
});
|
|
690
682
|
}
|
|
691
683
|
|
|
692
684
|
async function removeSupersededPlanningPipelineEntries(
|
|
@@ -698,7 +690,7 @@ async function removeSupersededPlanningPipelineEntries(
|
|
|
698
690
|
for (const skill of upstreamPlanningPipelineSkills(entry.skill)) {
|
|
699
691
|
await removeActiveEntry(cwd, sessionScope, skill, {
|
|
700
692
|
cwd,
|
|
701
|
-
audit: activeStateWriterAudit("remove-superseded-pipeline-entry"),
|
|
693
|
+
audit: activeStateWriterAudit("remove-superseded-pipeline-entry", sessionScope),
|
|
702
694
|
});
|
|
703
695
|
}
|
|
704
696
|
}
|
|
@@ -708,18 +700,17 @@ async function activeSubskillsForExistingEntry(
|
|
|
708
700
|
sessionId: string | undefined,
|
|
709
701
|
skill: string,
|
|
710
702
|
): Promise<ActiveSubskillEntry[] | undefined> {
|
|
711
|
-
const
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
]);
|
|
716
|
-
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(
|
|
717
707
|
entry => entry.skill === skill,
|
|
718
708
|
);
|
|
719
709
|
return existing?.active_subskills;
|
|
720
710
|
}
|
|
721
711
|
|
|
722
712
|
export async function syncSkillActiveState(options: SyncSkillActiveStateOptions): Promise<void> {
|
|
713
|
+
if (!options.sessionId) return;
|
|
723
714
|
const preservedActiveSubskills =
|
|
724
715
|
options.active_subskills === undefined
|
|
725
716
|
? await activeSubskillsForExistingEntry(options.cwd, options.sessionId, options.skill)
|
|
@@ -745,12 +736,8 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
745
736
|
: preservedActiveSubskills
|
|
746
737
|
? { active_subskills: preservedActiveSubskills }
|
|
747
738
|
: {}),
|
|
739
|
+
...(typeof options.sourceRevision === "number" ? { source_state_revision: options.sourceRevision } : {}),
|
|
748
740
|
};
|
|
749
|
-
await removeSupersededPlanningPipelineEntries(options.cwd, undefined, entry);
|
|
750
|
-
await persistActiveEntry(options.cwd, undefined, entry);
|
|
751
|
-
await rebuildActiveState(options.cwd);
|
|
752
|
-
|
|
753
|
-
if (!options.sessionId) return;
|
|
754
741
|
const sessionScope = { sessionId: options.sessionId };
|
|
755
742
|
await removeSupersededPlanningPipelineEntries(options.cwd, sessionScope, entry);
|
|
756
743
|
await persistActiveEntry(options.cwd, sessionScope, entry);
|
|
@@ -768,36 +755,23 @@ export interface ApplyHandoffOptions {
|
|
|
768
755
|
}
|
|
769
756
|
|
|
770
757
|
/**
|
|
771
|
-
* Atomically apply a workflow-skill handoff to
|
|
772
|
-
* root `skill-active-state.json` files in a single write per file.
|
|
773
|
-
*
|
|
774
|
-
* Write order: **session first, root last**. The session file is the
|
|
775
|
-
* source of truth for HUD; the root aggregate must never lead the session
|
|
776
|
-
* during a handoff window. Each file is rewritten once with caller demoted
|
|
777
|
-
* to `active:false` (preserving `handoff_to`/`handoff_at` lineage) and
|
|
778
|
-
* callee promoted to `active:true` (with `handoff_from`/`handoff_at`).
|
|
758
|
+
* Atomically apply a workflow-skill handoff to the session-scoped active state.
|
|
779
759
|
*/
|
|
780
760
|
export async function applyHandoffToActiveState(options: ApplyHandoffOptions): Promise<void> {
|
|
781
761
|
const nowIso = options.nowIso ?? new Date().toISOString();
|
|
782
762
|
const callerEntry = buildSyncEntry(options.caller, nowIso);
|
|
783
763
|
const calleeEntry = buildSyncEntry(options.callee, nowIso);
|
|
784
764
|
const sessionId = options.callee.sessionId ?? options.caller.sessionId;
|
|
785
|
-
|
|
765
|
+
assertNonEmptyGjcSessionId(sessionId, "applyHandoffToActiveState");
|
|
766
|
+
const { sessionPath } = getSkillActiveStatePaths(options.cwd, sessionId);
|
|
786
767
|
const readState = (filePath: string) => readRawActiveStateForHandoff(filePath, options.strict === true);
|
|
787
|
-
await
|
|
788
|
-
|
|
789
|
-
// A skill can hold more than one visible row in this session's scope — e.g.
|
|
790
|
-
// it was seeded without a session id (rendered globally) and is now handed
|
|
791
|
-
// off under a concrete session id. Supersede every same-session-scope row of
|
|
792
|
-
// the caller and callee skills, not just the exact `skill::session_id` key,
|
|
793
|
-
// so a stale `active:true` row cannot survive the demotion and keep showing
|
|
794
|
-
// in the HUD. Rows owned by other sessions are left untouched.
|
|
768
|
+
await readState(sessionPath);
|
|
769
|
+
|
|
795
770
|
const handoffSession = safeString(sessionId).trim();
|
|
796
771
|
const reassignedSkills = new Set([callerEntry.skill, calleeEntry.skill]);
|
|
797
772
|
const supersedesVisible = (entry: SkillActiveEntry): boolean => {
|
|
798
773
|
if (!reassignedSkills.has(entry.skill)) return false;
|
|
799
|
-
|
|
800
|
-
return entrySession.length === 0 || entrySession === handoffSession;
|
|
774
|
+
return safeString(entry.session_id).trim() === handoffSession;
|
|
801
775
|
};
|
|
802
776
|
const applyEntries = (entries: SkillActiveEntry[]): SkillActiveEntry[] => {
|
|
803
777
|
const callerKey = entryKey(callerEntry);
|
|
@@ -805,9 +779,6 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
805
779
|
entries.find(e => entryKey(e) === callerKey) ??
|
|
806
780
|
entries.find(e => e.skill === callerEntry.skill && supersedesVisible(e) && Boolean(e.handoff_from));
|
|
807
781
|
const kept = entries.filter(e => !supersedesVisible(e));
|
|
808
|
-
// Merge prior lineage into the demoted caller so multi-step handoff
|
|
809
|
-
// chains preserve `handoff_from` from the previous transition while
|
|
810
|
-
// the new `handoff_to`/`handoff_at` describe this one.
|
|
811
782
|
const mergedCaller: SkillActiveEntry = priorCaller
|
|
812
783
|
? {
|
|
813
784
|
...callerEntry,
|
|
@@ -822,10 +793,7 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
822
793
|
activeSubskills.length > 0 ? { ...calleeEntry, active_subskills: activeSubskills } : calleeEntry;
|
|
823
794
|
return [...kept, mergedCaller, mergedCallee];
|
|
824
795
|
};
|
|
825
|
-
const writeEntries = async (
|
|
826
|
-
sessionScope: ActiveSessionScope | undefined,
|
|
827
|
-
prior: SkillActiveState | null,
|
|
828
|
-
): Promise<void> => {
|
|
796
|
+
const writeEntries = async (sessionScope: ActiveSessionScope, prior: SkillActiveState | null): Promise<void> => {
|
|
829
797
|
const nextEntries = applyEntries(rawActiveEntries(prior));
|
|
830
798
|
for (const entry of nextEntries) {
|
|
831
799
|
await writeHandoffEntry(options.cwd, sessionScope, entry);
|
|
@@ -833,12 +801,8 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
833
801
|
await rebuildActiveState(options.cwd, sessionScope);
|
|
834
802
|
};
|
|
835
803
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
await writeEntries({ sessionId }, prior);
|
|
839
|
-
}
|
|
840
|
-
const priorRoot = await readState(rootPath);
|
|
841
|
-
await writeEntries(undefined, priorRoot);
|
|
804
|
+
const prior = await readState(sessionPath);
|
|
805
|
+
await writeEntries({ sessionId }, prior);
|
|
842
806
|
}
|
|
843
807
|
|
|
844
808
|
function buildSyncEntry(options: SyncSkillActiveStateOptions, nowIso: string): SkillActiveEntry {
|