@gajae-code/coding-agent 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +29 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +27 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +5 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli/setup-cli.ts +138 -3
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +193 -0
- package/src/config/file-lock.ts +66 -10
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +39 -30
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +232 -19
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +48 -30
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +177 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +14 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/docs-index.generated.ts +22 -12
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +18 -3
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +51 -8
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +81 -1
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +266 -34
- package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +29 -4
- package/src/session/agent-session.ts +99 -19
- package/src/session/blob-store.ts +59 -3
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +72 -20
- package/src/setup/credential-import.ts +429 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +16 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +59 -2
- package/src/tools/cron.ts +1 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +128 -29
- package/src/tools/subagent.ts +173 -9
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -5,9 +5,20 @@ import { syncSkillActiveState } from "../skill-state/active-state";
|
|
|
5
5
|
import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
|
|
6
6
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
7
7
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
8
|
+
import {
|
|
9
|
+
formatRalplanStagePresence,
|
|
10
|
+
parseRalplanIndexLine,
|
|
11
|
+
type RalplanIndexRow,
|
|
12
|
+
summarizeRalplanIndex,
|
|
13
|
+
} from "./ledger-event-renderer";
|
|
8
14
|
import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
9
15
|
import { migrateWorkflowState } from "./state-migrations";
|
|
10
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
appendJsonlIdempotent,
|
|
18
|
+
readExistingStateForMutation,
|
|
19
|
+
writeArtifact,
|
|
20
|
+
writeWorkflowEnvelopeAtomic,
|
|
21
|
+
} from "./state-writer";
|
|
11
22
|
|
|
12
23
|
/**
|
|
13
24
|
* Native implementation of `gjc ralplan`.
|
|
@@ -180,7 +191,37 @@ async function readActiveRunId(cwd: string, sessionId: string | undefined): Prom
|
|
|
180
191
|
return candidate;
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Run-state phases that an artifact write must never reopen. Once ralplan has
|
|
196
|
+
* reached a terminal/handed-off phase, a stray `--write` must not regress
|
|
197
|
+
* `current_phase` back to a stage — that would silently re-arm a chain guard or
|
|
198
|
+
* undo Stop semantics. Every other phase advances to track the stage just
|
|
199
|
+
* persisted so run-state stays coherent with the active ralplan stage.
|
|
200
|
+
*/
|
|
201
|
+
const PHASE_LOCK = new Set([
|
|
202
|
+
"final",
|
|
203
|
+
"handoff",
|
|
204
|
+
"complete",
|
|
205
|
+
"completed",
|
|
206
|
+
"failed",
|
|
207
|
+
"cancelled",
|
|
208
|
+
"canceled",
|
|
209
|
+
"inactive",
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
/** Phase that keeps run-state coherent with the stage just written, preserving locked phases. */
|
|
213
|
+
function advanceCurrentPhase(existingPhase: unknown, stage: RalplanStage): string {
|
|
214
|
+
const current = typeof existingPhase === "string" ? existingPhase.trim() : "";
|
|
215
|
+
if (current && PHASE_LOCK.has(current)) return current;
|
|
216
|
+
return stage;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function persistActiveRunId(
|
|
220
|
+
cwd: string,
|
|
221
|
+
sessionId: string | undefined,
|
|
222
|
+
runId: string,
|
|
223
|
+
stage: RalplanStage,
|
|
224
|
+
): Promise<void> {
|
|
184
225
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
185
226
|
const existingRead = await readExistingStateForMutation(statePath);
|
|
186
227
|
if (existingRead.kind === "corrupt") {
|
|
@@ -191,11 +232,25 @@ async function persistActiveRunId(cwd: string, sessionId: string | undefined, ru
|
|
|
191
232
|
}
|
|
192
233
|
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
193
234
|
|
|
194
|
-
|
|
235
|
+
// A new run_id is a fresh run, not a stray write on the prior run: never inherit a
|
|
236
|
+
// previous run's terminal/locked phase (which would start the new run already
|
|
237
|
+
// "complete"/"handoff" and disarm the Stop hook). PHASE_LOCK only guards same-run writes.
|
|
238
|
+
const isNewRun = existing.run_id !== runId;
|
|
239
|
+
const nextPhase = isNewRun ? stage : advanceCurrentPhase(existing.current_phase, stage);
|
|
240
|
+
if (
|
|
241
|
+
existing.run_id === runId &&
|
|
242
|
+
existing.version === WORKFLOW_STATE_VERSION &&
|
|
243
|
+
existing.current_phase === nextPhase &&
|
|
244
|
+
(existing.active === true || PHASE_LOCK.has(nextPhase))
|
|
245
|
+
) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
195
248
|
existing.run_id = runId;
|
|
196
249
|
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
197
|
-
|
|
198
|
-
|
|
250
|
+
// A successful persist means ralplan is actively writing this run's artifacts, so always
|
|
251
|
+
// re-assert active. Fallback-only init left active:false after a clear (#644, sibling of #638).
|
|
252
|
+
existing.active = true;
|
|
253
|
+
existing.current_phase = nextPhase;
|
|
199
254
|
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
200
255
|
existing.updated_at = new Date().toISOString();
|
|
201
256
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
@@ -375,8 +430,6 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
375
430
|
const explicitRunId = flagValue(args, "--run-id")?.trim();
|
|
376
431
|
const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
|
|
377
432
|
assertSafePathComponent(runId, "run-id");
|
|
378
|
-
// Persist the active run id so later writes in the same loop land in the same directory.
|
|
379
|
-
await persistActiveRunId(cwd, sessionId, runId);
|
|
380
433
|
|
|
381
434
|
const artifact = await resolveArtifactContent(rawArtifact, cwd);
|
|
382
435
|
return { stage: stage as RalplanStage, stageN, runId, artifact, sessionId, json: hasFlag(args, "--json") };
|
|
@@ -392,18 +445,34 @@ interface PersistedArtifact {
|
|
|
392
445
|
pendingApprovalPath?: string;
|
|
393
446
|
}
|
|
394
447
|
|
|
395
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Content-addressed identity for an `index.jsonl` row: a repeated `--write` of the
|
|
450
|
+
* same `(stage, stage_n)` at identical content (same sha256) is the #638 duplicate
|
|
451
|
+
* the append must collapse. Rows missing these fields opt out of dedup.
|
|
452
|
+
*/
|
|
453
|
+
function ralplanIndexKey(entry: unknown): string | undefined {
|
|
454
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return undefined;
|
|
455
|
+
const record = entry as Record<string, unknown>;
|
|
456
|
+
const { stage, stage_n, sha256 } = record;
|
|
457
|
+
if (typeof stage !== "string" || typeof stage_n !== "number" || typeof sha256 !== "string") return undefined;
|
|
458
|
+
return `${stage}\u0000${stage_n}\u0000${sha256}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function persistArtifact(
|
|
462
|
+
resolved: ResolvedArtifactArgs,
|
|
463
|
+
cwd: string,
|
|
464
|
+
content: string,
|
|
465
|
+
sha256: string,
|
|
466
|
+
): Promise<PersistedArtifact> {
|
|
396
467
|
const runDir = path.join(cwd, ".gjc", "plans", "ralplan", resolved.runId);
|
|
397
468
|
|
|
398
469
|
const fileName = `stage-${pad2(resolved.stageN)}-${resolved.stage}.md`;
|
|
399
470
|
const filePath = path.join(runDir, fileName);
|
|
400
|
-
const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
|
|
401
471
|
await writeArtifact(filePath, content, {
|
|
402
472
|
cwd,
|
|
403
473
|
audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
|
|
404
474
|
});
|
|
405
475
|
|
|
406
|
-
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
407
476
|
const createdAt = new Date().toISOString();
|
|
408
477
|
const indexEntry = {
|
|
409
478
|
stage: resolved.stage,
|
|
@@ -412,9 +481,10 @@ async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Pro
|
|
|
412
481
|
created_at: createdAt,
|
|
413
482
|
sha256,
|
|
414
483
|
};
|
|
415
|
-
await
|
|
484
|
+
await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
|
|
416
485
|
cwd,
|
|
417
486
|
audit: { category: "ledger", verb: "append", owner: "gjc-runtime", skill: "ralplan" },
|
|
487
|
+
key: ralplanIndexKey,
|
|
418
488
|
});
|
|
419
489
|
|
|
420
490
|
let pendingApprovalPath: string | undefined;
|
|
@@ -437,12 +507,82 @@ async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Pro
|
|
|
437
507
|
};
|
|
438
508
|
}
|
|
439
509
|
|
|
510
|
+
/** The persisted `(stage, stage_n)` artifact recorded in a run's `index.jsonl`. */
|
|
511
|
+
interface ExistingStageArtifact {
|
|
512
|
+
path: string;
|
|
513
|
+
sha256: string;
|
|
514
|
+
createdAt: string;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Find the most recent `index.jsonl` row for a `(stage, stage_n)` pair so a
|
|
519
|
+
* repeated `--write` can dedupe instead of silently clobbering the artifact and
|
|
520
|
+
* appending a duplicate ledger row. Best-effort: a missing or unreadable index
|
|
521
|
+
* yields `undefined`, treated as "no prior artifact". The ledger is the source of
|
|
522
|
+
* truth for dedup because it is exactly what a duplicate write would corrupt.
|
|
523
|
+
*/
|
|
524
|
+
async function findExistingStageArtifact(
|
|
525
|
+
cwd: string,
|
|
526
|
+
runId: string,
|
|
527
|
+
stage: RalplanStage,
|
|
528
|
+
stageN: number,
|
|
529
|
+
): Promise<ExistingStageArtifact | undefined> {
|
|
530
|
+
const indexPath = path.join(cwd, ".gjc", "plans", "ralplan", runId, "index.jsonl");
|
|
531
|
+
let text: string;
|
|
532
|
+
try {
|
|
533
|
+
text = await fs.readFile(indexPath, "utf8");
|
|
534
|
+
} catch {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
let match: ExistingStageArtifact | undefined;
|
|
538
|
+
for (const line of text.split(/\r?\n/)) {
|
|
539
|
+
const trimmed = line.trim();
|
|
540
|
+
if (!trimmed) continue;
|
|
541
|
+
let row: unknown;
|
|
542
|
+
try {
|
|
543
|
+
row = JSON.parse(trimmed);
|
|
544
|
+
} catch {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
548
|
+
const record = row as Record<string, unknown>;
|
|
549
|
+
if (record.stage !== stage || record.stage_n !== stageN) continue;
|
|
550
|
+
if (typeof record.path !== "string" || typeof record.sha256 !== "string") continue;
|
|
551
|
+
match = {
|
|
552
|
+
path: record.path,
|
|
553
|
+
sha256: record.sha256,
|
|
554
|
+
createdAt: typeof record.created_at === "string" ? record.created_at : "",
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return match;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Read and parse the run's `index.jsonl` rows. Best-effort: returns [] when the
|
|
562
|
+
* file is absent or unreadable so HUD sync never fails on a missing index.
|
|
563
|
+
*/
|
|
564
|
+
async function readRalplanIndexRows(cwd: string, runId: string): Promise<RalplanIndexRow[]> {
|
|
565
|
+
try {
|
|
566
|
+
const indexPath = path.join(cwd, ".gjc", "plans", "ralplan", runId, "index.jsonl");
|
|
567
|
+
const text = await fs.readFile(indexPath, "utf8");
|
|
568
|
+
const rows: RalplanIndexRow[] = [];
|
|
569
|
+
for (const line of text.split(/\r?\n/)) {
|
|
570
|
+
const row = parseRalplanIndexLine(line);
|
|
571
|
+
if (row) rows.push(row);
|
|
572
|
+
}
|
|
573
|
+
return rows;
|
|
574
|
+
} catch {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
440
579
|
async function syncRalplanHud(options: {
|
|
441
580
|
cwd: string;
|
|
442
581
|
sessionId?: string;
|
|
443
582
|
stage: string;
|
|
444
583
|
pendingApproval: boolean;
|
|
445
584
|
iteration?: number;
|
|
585
|
+
runId?: string;
|
|
446
586
|
latestSummary?: string;
|
|
447
587
|
}): Promise<void> {
|
|
448
588
|
try {
|
|
@@ -453,23 +593,65 @@ async function syncRalplanHud(options: {
|
|
|
453
593
|
phase: options.stage,
|
|
454
594
|
sessionId: options.sessionId,
|
|
455
595
|
source: "gjc-ralplan-native",
|
|
456
|
-
hud:
|
|
457
|
-
stage: options.stage,
|
|
458
|
-
iteration: options.iteration,
|
|
459
|
-
pendingApproval: options.pendingApproval,
|
|
460
|
-
latestSummary: options.latestSummary,
|
|
461
|
-
updatedAt: new Date().toISOString(),
|
|
462
|
-
}),
|
|
596
|
+
hud: await buildRalplanHud(options),
|
|
463
597
|
});
|
|
464
598
|
} catch {
|
|
465
599
|
// HUD sync is best-effort and must not change command semantics.
|
|
466
600
|
}
|
|
467
601
|
}
|
|
468
602
|
|
|
603
|
+
async function buildRalplanHud(options: {
|
|
604
|
+
cwd: string;
|
|
605
|
+
stage: string;
|
|
606
|
+
pendingApproval: boolean;
|
|
607
|
+
iteration?: number;
|
|
608
|
+
latestSummary?: string;
|
|
609
|
+
runId?: string;
|
|
610
|
+
}) {
|
|
611
|
+
let iterationFromIndex: number | undefined;
|
|
612
|
+
let stages: string | undefined;
|
|
613
|
+
if (options.runId) {
|
|
614
|
+
const rows = await readRalplanIndexRows(options.cwd, options.runId);
|
|
615
|
+
if (rows.length > 0) {
|
|
616
|
+
const summary = summarizeRalplanIndex(rows);
|
|
617
|
+
iterationFromIndex = summary.iteration;
|
|
618
|
+
stages = formatRalplanStagePresence(summary.currentStages);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return buildRalplanHudSummary({
|
|
622
|
+
stage: options.stage,
|
|
623
|
+
iteration: options.iteration,
|
|
624
|
+
iterationFromIndex,
|
|
625
|
+
stages,
|
|
626
|
+
pendingApproval: options.pendingApproval,
|
|
627
|
+
latestSummary: options.latestSummary,
|
|
628
|
+
updatedAt: new Date().toISOString(),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
469
632
|
async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
470
633
|
const plannerState = parsePlannerStateArgs(args);
|
|
471
634
|
const resolved = await resolveArtifactArgs(args, cwd);
|
|
472
|
-
const
|
|
635
|
+
const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
|
|
636
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
637
|
+
|
|
638
|
+
// Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
|
|
639
|
+
// silently clobber the artifact or append a duplicate ledger row. Classify before any
|
|
640
|
+
// state mutation so a conflict never regresses run-state phase.
|
|
641
|
+
const existingArtifact = await findExistingStageArtifact(cwd, resolved.runId, resolved.stage, resolved.stageN);
|
|
642
|
+
if (existingArtifact) {
|
|
643
|
+
if (existingArtifact.sha256 !== sha256) {
|
|
644
|
+
throw new RalplanCommandError(
|
|
645
|
+
2,
|
|
646
|
+
`refusing to overwrite ralplan ${resolved.stage} stage ${resolved.stageN} at ${existingArtifact.path}: an artifact with different content already exists (existing sha256=${existingArtifact.sha256}, new sha256=${sha256}). Use a new --stage_n to record another pass.`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Keep run-state `current_phase` coherent with the stage being persisted.
|
|
653
|
+
await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
|
|
654
|
+
const persisted = await persistArtifact(resolved, cwd, content, sha256);
|
|
473
655
|
if (plannerState) {
|
|
474
656
|
await applyPlannerStateUpdate(cwd, resolved.sessionId, plannerState);
|
|
475
657
|
}
|
|
@@ -477,6 +659,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
477
659
|
cwd,
|
|
478
660
|
sessionId: resolved.sessionId,
|
|
479
661
|
stage: persisted.stage,
|
|
662
|
+
runId: persisted.runId,
|
|
480
663
|
pendingApproval: persisted.stage === "final",
|
|
481
664
|
iteration: persisted.stageN,
|
|
482
665
|
latestSummary: `persisted ${persisted.stage} stage ${persisted.stageN}`,
|
|
@@ -497,6 +680,35 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
497
680
|
return { status: 0, stdout };
|
|
498
681
|
}
|
|
499
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Deterministic no-op receipt for an identical repeated `--write`: report the
|
|
685
|
+
* already-persisted artifact without rewriting the file, appending a ledger row, or
|
|
686
|
+
* churning run-state. `deduplicated: true` lets callers distinguish it from a fresh write.
|
|
687
|
+
*/
|
|
688
|
+
function buildDeduplicatedResult(
|
|
689
|
+
resolved: ResolvedArtifactArgs,
|
|
690
|
+
existing: ExistingStageArtifact,
|
|
691
|
+
sha256: string,
|
|
692
|
+
cwd: string,
|
|
693
|
+
): RalplanCommandResult {
|
|
694
|
+
const payload: Record<string, unknown> = {
|
|
695
|
+
run_id: resolved.runId,
|
|
696
|
+
path: existing.path,
|
|
697
|
+
stage: resolved.stage,
|
|
698
|
+
stage_n: resolved.stageN,
|
|
699
|
+
sha256,
|
|
700
|
+
created_at: existing.createdAt,
|
|
701
|
+
deduplicated: true,
|
|
702
|
+
};
|
|
703
|
+
if (resolved.stage === "final") {
|
|
704
|
+
payload.pending_approval_path = path.join(cwd, ".gjc", "plans", "ralplan", resolved.runId, "pending-approval.md");
|
|
705
|
+
}
|
|
706
|
+
const stdout = resolved.json
|
|
707
|
+
? `${JSON.stringify(payload, null, 2)}\n`
|
|
708
|
+
: `ralplan ${resolved.stage} stage ${resolved.stageN} already persisted at ${existing.path} (identical content; no changes written).\n`;
|
|
709
|
+
return { status: 0, stdout };
|
|
710
|
+
}
|
|
711
|
+
|
|
500
712
|
/* -------------------------------- handoff -------------------------------- */
|
|
501
713
|
|
|
502
714
|
interface ConsensusHandoffArgs {
|
|
@@ -617,6 +829,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
617
829
|
cwd,
|
|
618
830
|
sessionId: resolved.sessionId,
|
|
619
831
|
stage: "planner",
|
|
832
|
+
runId,
|
|
620
833
|
pendingApproval: false,
|
|
621
834
|
iteration: 1,
|
|
622
835
|
latestSummary: `${mode} run · ${resolved.interactive ? "interactive" : "automated"}`,
|
|
@@ -118,8 +118,13 @@ export interface StateStatusSummary {
|
|
|
118
118
|
|
|
119
119
|
function compactStateFields(state: Record<string, unknown>): Array<[string, string]> {
|
|
120
120
|
const fields: Array<[string, string]> = [];
|
|
121
|
+
const nested = isRecord(state.state) ? state.state : undefined;
|
|
121
122
|
for (const key of COMPACT_ELIDE_KEYS) {
|
|
122
|
-
const value = state[key]
|
|
123
|
+
const value = Array.isArray(state[key])
|
|
124
|
+
? state[key]
|
|
125
|
+
: nested && Array.isArray(nested[key])
|
|
126
|
+
? nested[key]
|
|
127
|
+
: undefined;
|
|
123
128
|
if (Array.isArray(value)) fields.push([key, `${value.length} entries (elided)`]);
|
|
124
129
|
}
|
|
125
130
|
return fields;
|
|
@@ -133,9 +138,13 @@ export function compactProjectStateJson(
|
|
|
133
138
|
const state = stateObject(stateJson);
|
|
134
139
|
const compact = projectStateFields(skill, stateJson, manifest, STATE_FIELD_ALLOWLIST);
|
|
135
140
|
const elisions: Record<string, unknown> = {};
|
|
141
|
+
const nested = isRecord(state.state) ? state.state : undefined;
|
|
136
142
|
for (const key of COMPACT_ELIDE_KEYS) {
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
if (Array.isArray(state[key])) {
|
|
144
|
+
elisions[key] = { type: "array", count: (state[key] as unknown[]).length, pointer: `/${key}` };
|
|
145
|
+
} else if (nested && Array.isArray(nested[key])) {
|
|
146
|
+
elisions[key] = { type: "array", count: (nested[key] as unknown[]).length, pointer: `/state/${key}` };
|
|
147
|
+
}
|
|
139
148
|
}
|
|
140
149
|
if (Object.keys(elisions).length) compact.elided = elisions;
|
|
141
150
|
return compact;
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
} from "../skill-state/active-state";
|
|
12
12
|
import { initialPhaseForSkill } from "../skill-state/initial-phase";
|
|
13
13
|
import {
|
|
14
|
-
buildDeepInterviewHudSummary,
|
|
15
14
|
buildRalplanHudSummary,
|
|
16
15
|
buildTeamHudSummary,
|
|
17
16
|
buildUltragoalHudSummary,
|
|
17
|
+
deriveDeepInterviewHud,
|
|
18
18
|
} from "../skill-state/workflow-hud";
|
|
19
19
|
import {
|
|
20
20
|
type AuditEntry,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type WorkflowStateReceipt,
|
|
27
27
|
} from "../skill-state/workflow-state-contract";
|
|
28
28
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
29
|
+
import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
|
|
29
30
|
import { renderStateGraph, type StateGraphFormat } from "./state-graph";
|
|
30
31
|
import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
|
|
31
32
|
import {
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
type StateWriterAuditContext,
|
|
52
53
|
softDelete,
|
|
53
54
|
updateWorkflowTransactionJournal,
|
|
55
|
+
type WorkflowEnvelopeIntegrityMismatch,
|
|
54
56
|
writeWorkflowEnvelopeAtomic,
|
|
55
57
|
} from "./state-writer";
|
|
56
58
|
import { getSkillManifest, isKnownWorkflowState, isValidTransition, typedArgsFor } from "./workflow-manifest";
|
|
@@ -658,7 +660,7 @@ async function warnAndAuditOutOfBandIfNeeded(
|
|
|
658
660
|
skill: CanonicalGjcWorkflowSkill,
|
|
659
661
|
options?: { mutationId?: string; forced?: boolean },
|
|
660
662
|
): Promise<string | undefined> {
|
|
661
|
-
let mismatch:
|
|
663
|
+
let mismatch: WorkflowEnvelopeIntegrityMismatch | undefined;
|
|
662
664
|
try {
|
|
663
665
|
mismatch = await detectWorkflowEnvelopeIntegrityMismatch(filePath);
|
|
664
666
|
} catch {
|
|
@@ -833,31 +835,9 @@ function buildHudForMode(
|
|
|
833
835
|
): WorkflowHudSummary | undefined {
|
|
834
836
|
const updatedAt = new Date().toISOString();
|
|
835
837
|
const phase = typeof payload.current_phase === "string" ? payload.current_phase : undefined;
|
|
836
|
-
const stateField = isPlainObject(payload.state) ? (payload.state as Record<string, unknown>) : {};
|
|
837
838
|
switch (mode) {
|
|
838
|
-
case "deep-interview":
|
|
839
|
-
|
|
840
|
-
const v = (stateField as Record<string, unknown>)[key] ?? (payload as Record<string, unknown>)[key];
|
|
841
|
-
return guard(v) ? v : undefined;
|
|
842
|
-
};
|
|
843
|
-
const isNumber = (v: unknown): v is number => typeof v === "number";
|
|
844
|
-
const isString = (v: unknown): v is string => typeof v === "string";
|
|
845
|
-
const isArray = (v: unknown): v is unknown[] => Array.isArray(v);
|
|
846
|
-
const ambiguity = pick("current_ambiguity", isNumber);
|
|
847
|
-
const threshold = pick("threshold", isNumber);
|
|
848
|
-
const rounds = pick("rounds", isArray);
|
|
849
|
-
const targetComponent = pick("last_targeted_component_id", isString);
|
|
850
|
-
const weakestDimension = pick("weakest_dimension", isString);
|
|
851
|
-
return buildDeepInterviewHudSummary({
|
|
852
|
-
phase,
|
|
853
|
-
ambiguity,
|
|
854
|
-
threshold,
|
|
855
|
-
roundCount: rounds?.length,
|
|
856
|
-
targetComponent,
|
|
857
|
-
weakestDimension,
|
|
858
|
-
updatedAt,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
839
|
+
case "deep-interview":
|
|
840
|
+
return deriveDeepInterviewHud(payload, { updatedAt });
|
|
861
841
|
case "ralplan": {
|
|
862
842
|
const stage =
|
|
863
843
|
typeof payload.current_phase === "string"
|
|
@@ -888,6 +868,24 @@ function buildHudForMode(
|
|
|
888
868
|
counts[status] = (counts[status] ?? 0) + 1;
|
|
889
869
|
}
|
|
890
870
|
const currentGoalRaw = goals.find(g => g.status === "active") ?? goals.find(g => g.status === "pending");
|
|
871
|
+
const rawLedger = payload.latestLedgerEvent;
|
|
872
|
+
const latestLedgerEvent =
|
|
873
|
+
rawLedger && typeof rawLedger === "object" && !Array.isArray(rawLedger)
|
|
874
|
+
? {
|
|
875
|
+
event:
|
|
876
|
+
typeof (rawLedger as Record<string, unknown>).event === "string"
|
|
877
|
+
? ((rawLedger as Record<string, unknown>).event as string)
|
|
878
|
+
: undefined,
|
|
879
|
+
goalId:
|
|
880
|
+
typeof (rawLedger as Record<string, unknown>).goalId === "string"
|
|
881
|
+
? ((rawLedger as Record<string, unknown>).goalId as string)
|
|
882
|
+
: undefined,
|
|
883
|
+
timestamp:
|
|
884
|
+
typeof (rawLedger as Record<string, unknown>).timestamp === "string"
|
|
885
|
+
? ((rawLedger as Record<string, unknown>).timestamp as string)
|
|
886
|
+
: undefined,
|
|
887
|
+
}
|
|
888
|
+
: undefined;
|
|
891
889
|
const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
|
|
892
890
|
return buildUltragoalHudSummary({
|
|
893
891
|
status,
|
|
@@ -900,6 +898,7 @@ function buildHudForMode(
|
|
|
900
898
|
: undefined,
|
|
901
899
|
counts,
|
|
902
900
|
goals: goals.map(g => ({ id: g.id as string, title: g.title as string, status: g.status as string })),
|
|
901
|
+
latestLedgerEvent,
|
|
903
902
|
updatedAt,
|
|
904
903
|
});
|
|
905
904
|
}
|
|
@@ -1009,7 +1008,10 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1009
1008
|
receipt.from_phase = fromPhase;
|
|
1010
1009
|
receipt.to_phase = trimmedPhase;
|
|
1011
1010
|
|
|
1012
|
-
const merged =
|
|
1011
|
+
const merged =
|
|
1012
|
+
mode === "deep-interview"
|
|
1013
|
+
? (mergeDeepInterviewEnvelope(existingPayload, payload) as Record<string, unknown>)
|
|
1014
|
+
: mergeWithNullDelete(existingPayload, payload);
|
|
1013
1015
|
merged.skill = mode;
|
|
1014
1016
|
merged.current_phase = trimmedPhase;
|
|
1015
1017
|
merged.active = active;
|
|
@@ -1173,7 +1175,11 @@ async function handleWrite(
|
|
|
1173
1175
|
? (innerState.current_phase as string).trim()
|
|
1174
1176
|
: undefined;
|
|
1175
1177
|
let merged: Record<string, unknown>;
|
|
1176
|
-
if (
|
|
1178
|
+
if (mode === "deep-interview") {
|
|
1179
|
+
// Deep-interview keeps interview data nested under `state` and merges rounds
|
|
1180
|
+
// losslessly by durable key; never flatten or delete `state` (that drops recorder history).
|
|
1181
|
+
merged = mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") });
|
|
1182
|
+
} else if (hasFlag(args, "--replace")) {
|
|
1177
1183
|
merged = { ...payload };
|
|
1178
1184
|
} else {
|
|
1179
1185
|
merged = mergeWithNullDelete(existingPayload, payload);
|
|
@@ -1423,8 +1429,20 @@ async function handleHandoff(
|
|
|
1423
1429
|
});
|
|
1424
1430
|
|
|
1425
1431
|
const calleeInitial = initialPhaseForSkill(callee);
|
|
1426
|
-
const normalizedCaller =
|
|
1427
|
-
|
|
1432
|
+
const normalizedCaller =
|
|
1433
|
+
caller === "deep-interview"
|
|
1434
|
+
? (normalizeDeepInterviewEnvelope(migrateWorkflowState(existingCaller, caller).state) as Record<
|
|
1435
|
+
string,
|
|
1436
|
+
unknown
|
|
1437
|
+
>)
|
|
1438
|
+
: migrateWorkflowState(existingCaller, caller).state;
|
|
1439
|
+
const normalizedCallee =
|
|
1440
|
+
callee === "deep-interview"
|
|
1441
|
+
? (normalizeDeepInterviewEnvelope(migrateWorkflowState(existingCallee, callee).state) as Record<
|
|
1442
|
+
string,
|
|
1443
|
+
unknown
|
|
1444
|
+
>)
|
|
1445
|
+
: migrateWorkflowState(existingCallee, callee).state;
|
|
1428
1446
|
const mergedCalleeState: Record<string, unknown> = {
|
|
1429
1447
|
...normalizedCallee,
|
|
1430
1448
|
skill: callee,
|