@gajae-code/coding-agent 0.11.10 → 0.12.0
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 +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +107 -7
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
|
@@ -1,843 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import * as fs from "node:fs/promises";
|
|
4
|
-
import * as os from "node:os";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
DEEP_INTERVIEW_DRAFT_KINDS,
|
|
8
|
-
type DeepInterviewDraftKind,
|
|
9
|
-
type DraftArrayDescriptor,
|
|
10
|
-
type DraftDescriptor,
|
|
11
|
-
type DraftLeafDescriptor,
|
|
12
|
-
type DraftObjectDescriptor,
|
|
13
|
-
deepInterviewDraftSchema,
|
|
14
|
-
validateDraftPayload,
|
|
15
|
-
} from "./deep-interview-payload";
|
|
16
|
-
import { type DeepInterviewRepairResult, runDeepInterviewRepairCommand } from "./deep-interview-repair";
|
|
17
|
-
import { canonicalDeepInterviewJson, validateDeepInterviewV1Envelope } from "./deep-interview-state";
|
|
18
|
-
import { modeStatePath } from "./session-layout";
|
|
19
|
-
import {
|
|
20
|
-
isNativeDeepInterviewV1,
|
|
21
|
-
readExistingStateForMutation,
|
|
22
|
-
transformGuardedWorkflowEnvelopeAtomic,
|
|
23
|
-
verifyWorkflowEnvelopeReceiptValue,
|
|
24
|
-
} from "./state-writer";
|
|
25
|
-
|
|
26
|
-
const DRAFT_VERSION = 1;
|
|
27
|
-
const ACTIVE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
28
|
-
const CONSUMED_MS = 24 * 60 * 60 * 1000;
|
|
29
|
-
const LOCK_STALE_MS = 30_000;
|
|
30
|
-
const LOCK_ATTEMPTS = 50;
|
|
31
|
-
const MAX_DRAFT_BYTES = 256 * 1024;
|
|
32
|
-
const MAX_SCAN_FILES = 256;
|
|
33
|
-
const MAX_SCAN_BYTES = 16 * 1024 * 1024;
|
|
34
|
-
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
35
|
-
const DRAFT_ID = /^[a-f0-9]{32}$/;
|
|
36
|
-
const FORBIDDEN = new Set(["__proto__", "prototype", "constructor"]);
|
|
37
|
-
|
|
38
|
-
type Scalar = string | number | boolean | null;
|
|
39
|
-
interface Draft {
|
|
40
|
-
version: 1;
|
|
41
|
-
id: string;
|
|
42
|
-
workspace: string;
|
|
43
|
-
session_id: string;
|
|
44
|
-
kind: DeepInterviewDraftKind;
|
|
45
|
-
base_revision: number;
|
|
46
|
-
draft_revision: number;
|
|
47
|
-
created_at: string;
|
|
48
|
-
updated_at: string;
|
|
49
|
-
expires_at: string;
|
|
50
|
-
status: "active" | "consumed";
|
|
51
|
-
round_key?: string;
|
|
52
|
-
identity: Record<string, string>;
|
|
53
|
-
payload: Record<string, unknown>;
|
|
54
|
-
receipt?: Record<string, unknown>;
|
|
55
|
-
attempt?: { state_revision: number; request_digest: string; effect_digest: string };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface DeepInterviewDraftResult {
|
|
59
|
-
status: number;
|
|
60
|
-
stdout?: string;
|
|
61
|
-
stderr?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function response(status: number, value: unknown): DeepInterviewDraftResult {
|
|
65
|
-
return status === 0
|
|
66
|
-
? { status, stdout: `${JSON.stringify(value)}\n` }
|
|
67
|
-
: { status, stderr: `${JSON.stringify({ ok: false, issue: { code: value, message: value } })}\n` };
|
|
68
|
-
}
|
|
69
|
-
function code(error: unknown): string {
|
|
70
|
-
return error instanceof Error ? error.message : "DI_DRAFT_INTERNAL_ERROR";
|
|
71
|
-
}
|
|
72
|
-
function inside(parent: string, child: string): boolean {
|
|
73
|
-
const relative = path.relative(parent, child);
|
|
74
|
-
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function safeDirectory(target: string, create: boolean): Promise<string> {
|
|
78
|
-
if (!path.isAbsolute(target)) throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
79
|
-
if (create) await fs.mkdir(target, { recursive: true, mode: 0o700 });
|
|
80
|
-
let metadata: Awaited<ReturnType<typeof fs.lstat>>;
|
|
81
|
-
try {
|
|
82
|
-
metadata = await fs.lstat(target);
|
|
83
|
-
} catch {
|
|
84
|
-
throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
85
|
-
}
|
|
86
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
87
|
-
const uid = process.getuid?.();
|
|
88
|
-
if (uid !== undefined && metadata.uid !== uid) throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
89
|
-
try {
|
|
90
|
-
await fs.chmod(target, 0o700);
|
|
91
|
-
metadata = await fs.lstat(target);
|
|
92
|
-
} catch {
|
|
93
|
-
throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
94
|
-
}
|
|
95
|
-
if ((metadata.mode & 0o077) !== 0) throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
96
|
-
try {
|
|
97
|
-
return await fs.realpath(target);
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function root(cwd: string): Promise<string> {
|
|
104
|
-
const configured = process.env.GJC_DEEP_INTERVIEW_DRAFT_ROOT;
|
|
105
|
-
const candidate = configured === undefined ? path.join(os.tmpdir(), "gjc-deep-interview-drafts") : configured;
|
|
106
|
-
if (!path.isAbsolute(candidate)) throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
107
|
-
const workspace = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
108
|
-
const workspaceGjc = path.join(workspace, ".gjc");
|
|
109
|
-
const storage = await safeDirectory(path.resolve(candidate), true);
|
|
110
|
-
if (inside(workspace, storage) || inside(workspaceGjc, storage)) throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
111
|
-
return storage;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function workspaceKey(cwd: string): string {
|
|
115
|
-
return createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 32);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function dir(cwd: string): Promise<string> {
|
|
119
|
-
const storage = await root(cwd);
|
|
120
|
-
const directory = path.join(storage, workspaceKey(cwd));
|
|
121
|
-
const workspace = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
122
|
-
const workspaceGjc = path.join(workspace, ".gjc");
|
|
123
|
-
const resolved = await safeDirectory(directory, true);
|
|
124
|
-
if (inside(workspace, resolved) || inside(workspaceGjc, resolved) || !inside(storage, resolved))
|
|
125
|
-
throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
126
|
-
return resolved;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function file(cwd: string, id: string): Promise<string> {
|
|
130
|
-
if (!DRAFT_ID.test(id)) throw new Error("DI_INVALID_DRAFT_ID");
|
|
131
|
-
return path.join(await dir(cwd), `${id}.json`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function safeFile(target: string): Promise<Awaited<ReturnType<typeof fs.lstat>>> {
|
|
135
|
-
let metadata: Awaited<ReturnType<typeof fs.lstat>>;
|
|
136
|
-
try {
|
|
137
|
-
metadata = await fs.lstat(target);
|
|
138
|
-
} catch {
|
|
139
|
-
throw new Error("DI_DRAFT_NOT_FOUND");
|
|
140
|
-
}
|
|
141
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_DRAFT_BYTES)
|
|
142
|
-
throw new Error("DI_DRAFT_CORRUPT");
|
|
143
|
-
const uid = process.getuid?.();
|
|
144
|
-
if (uid !== undefined && metadata.uid !== uid) throw new Error("DI_DRAFT_CORRUPT");
|
|
145
|
-
if ((metadata.mode & 0o077) !== 0) throw new Error("DI_DRAFT_CORRUPT");
|
|
146
|
-
return metadata;
|
|
147
|
-
}
|
|
148
|
-
async function secureRead(target: string, limit: number, errorCode: string): Promise<string> {
|
|
149
|
-
let handle: fs.FileHandle | undefined;
|
|
150
|
-
try {
|
|
151
|
-
handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
152
|
-
const metadata = await handle.stat();
|
|
153
|
-
const uid = process.getuid?.();
|
|
154
|
-
if (
|
|
155
|
-
!metadata.isFile() ||
|
|
156
|
-
metadata.size > limit ||
|
|
157
|
-
(uid !== undefined && metadata.uid !== uid) ||
|
|
158
|
-
(metadata.mode & 0o077) !== 0
|
|
159
|
-
)
|
|
160
|
-
throw new Error(errorCode);
|
|
161
|
-
return await handle.readFile({ encoding: "utf8" });
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (error instanceof Error && error.message === errorCode) throw error;
|
|
164
|
-
throw new Error(errorCode);
|
|
165
|
-
} finally {
|
|
166
|
-
await handle?.close().catch(() => undefined);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function withLock<T>(cwd: string, fn: () => Promise<T>): Promise<T> {
|
|
171
|
-
const directory = await dir(cwd);
|
|
172
|
-
const lock = path.join(directory, ".lock");
|
|
173
|
-
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt++) {
|
|
174
|
-
try {
|
|
175
|
-
const handle = await fs.open(lock, "wx", 0o600);
|
|
176
|
-
const created = await handle.stat();
|
|
177
|
-
try {
|
|
178
|
-
return await fn();
|
|
179
|
-
} finally {
|
|
180
|
-
await handle.close();
|
|
181
|
-
const current = await fs.lstat(lock).catch(() => undefined);
|
|
182
|
-
if (
|
|
183
|
-
current?.isFile() &&
|
|
184
|
-
!current.isSymbolicLink() &&
|
|
185
|
-
current.dev === created.dev &&
|
|
186
|
-
current.ino === created.ino
|
|
187
|
-
)
|
|
188
|
-
await fs.unlink(lock).catch(() => undefined);
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
192
|
-
const current = await fs.lstat(lock).catch(() => undefined);
|
|
193
|
-
if (!current?.isFile() || current.isSymbolicLink()) throw new Error("DI_DRAFT_UNSAFE_ROOT");
|
|
194
|
-
if (Date.now() - current.mtimeMs > LOCK_STALE_MS) await fs.unlink(lock).catch(() => undefined);
|
|
195
|
-
else await Bun.sleep(10);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
throw new Error("DI_DRAFT_LOCK_TIMEOUT");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function cleanup(cwd: string): Promise<void> {
|
|
202
|
-
const directory = await dir(cwd);
|
|
203
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
204
|
-
if (entries.length > MAX_SCAN_FILES) throw new Error("DI_DRAFT_STORAGE_QUOTA");
|
|
205
|
-
let total = 0;
|
|
206
|
-
const now = Date.now();
|
|
207
|
-
for (const entry of entries) {
|
|
208
|
-
if (entry.name === ".lock") continue;
|
|
209
|
-
const target = path.join(directory, entry.name);
|
|
210
|
-
const metadata = await fs.lstat(target);
|
|
211
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error("DI_DRAFT_STORAGE_QUOTA");
|
|
212
|
-
total += metadata.size;
|
|
213
|
-
if (total > MAX_SCAN_BYTES) throw new Error("DI_DRAFT_STORAGE_QUOTA");
|
|
214
|
-
if (!entry.name.endsWith(".json")) continue;
|
|
215
|
-
if (metadata.size > MAX_DRAFT_BYTES) throw new Error("DI_DRAFT_STORAGE_QUOTA");
|
|
216
|
-
try {
|
|
217
|
-
const value = JSON.parse(await secureRead(target, MAX_DRAFT_BYTES, "DI_DRAFT_STORAGE_QUOTA")) as Draft;
|
|
218
|
-
const age = now - Date.parse(value.updated_at);
|
|
219
|
-
if (age > (value.status === "consumed" ? CONSUMED_MS : ACTIVE_MS)) await fs.unlink(target);
|
|
220
|
-
} catch {
|
|
221
|
-
await fs.unlink(target).catch(() => undefined);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function write(cwd: string, draft: Draft): Promise<void> {
|
|
227
|
-
const target = await file(cwd, draft.id);
|
|
228
|
-
const temporary = `${target}.${randomBytes(8).toString("hex")}.tmp`;
|
|
229
|
-
let handle: fs.FileHandle | undefined;
|
|
230
|
-
try {
|
|
231
|
-
handle = await fs.open(temporary, "wx", 0o600);
|
|
232
|
-
await handle.writeFile(JSON.stringify(draft), "utf8");
|
|
233
|
-
await handle.chmod(0o600);
|
|
234
|
-
await handle.close();
|
|
235
|
-
handle = undefined;
|
|
236
|
-
await fs.rename(temporary, target);
|
|
237
|
-
const metadata = await safeFile(target);
|
|
238
|
-
if (metadata.size > MAX_DRAFT_BYTES) throw new Error("DI_DRAFT_STORAGE_QUOTA");
|
|
239
|
-
} finally {
|
|
240
|
-
await handle?.close().catch(() => undefined);
|
|
241
|
-
await fs.unlink(temporary).catch(() => undefined);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function read(cwd: string, id: string): Promise<Draft> {
|
|
246
|
-
const target = await file(cwd, id);
|
|
247
|
-
await safeFile(target);
|
|
248
|
-
let value: unknown;
|
|
249
|
-
try {
|
|
250
|
-
value = JSON.parse(await secureRead(target, MAX_DRAFT_BYTES, "DI_DRAFT_NOT_FOUND"));
|
|
251
|
-
} catch {
|
|
252
|
-
throw new Error("DI_DRAFT_NOT_FOUND");
|
|
253
|
-
}
|
|
254
|
-
if (!value || typeof value !== "object") throw new Error("DI_DRAFT_CORRUPT");
|
|
255
|
-
const draft = value as Draft;
|
|
256
|
-
if (draft.version !== DRAFT_VERSION || draft.workspace !== path.resolve(cwd)) throw new Error("DI_DRAFT_NOT_FOUND");
|
|
257
|
-
if (draft.status === "active" && Date.parse(draft.expires_at) <= Date.now()) throw new Error("DI_DRAFT_EXPIRED");
|
|
258
|
-
return draft;
|
|
259
|
-
}
|
|
260
|
-
function args(input: readonly string[]): Map<string, string> {
|
|
261
|
-
const flags = new Map<string, string>();
|
|
262
|
-
for (let i = 1; i < input.length; i++) {
|
|
263
|
-
const key = input[i];
|
|
264
|
-
if (!key.startsWith("--") || key.includes("=") || flags.has(key)) throw new Error("DI_INVALID_ARGUMENT");
|
|
265
|
-
if (key === "--null") {
|
|
266
|
-
flags.set(key, "true");
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
if (key === "--json") {
|
|
270
|
-
// draftCommandArgs normalizes the native router's standalone --json to --json true.
|
|
271
|
-
if (input[i + 1] === "true") i++;
|
|
272
|
-
flags.set(key, "true");
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
const value = input[++i];
|
|
276
|
-
if (value === undefined || value.startsWith("--")) throw new Error("DI_INVALID_ARGUMENT");
|
|
277
|
-
flags.set(key, value);
|
|
278
|
-
}
|
|
279
|
-
return flags;
|
|
280
|
-
}
|
|
281
|
-
function allow(flags: Map<string, string>, allowed: readonly string[]): void {
|
|
282
|
-
for (const key of flags.keys()) if (!allowed.includes(key)) throw new Error("DI_INVALID_ARGUMENT");
|
|
283
|
-
}
|
|
284
|
-
function required(flags: Map<string, string>, name: string): string {
|
|
285
|
-
const value = flags.get(name);
|
|
286
|
-
if (!value) throw new Error("DI_INVALID_ARGUMENT");
|
|
287
|
-
return value;
|
|
288
|
-
}
|
|
289
|
-
async function revision(cwd: string, session: string): Promise<{ revision: number; state: Record<string, unknown> }> {
|
|
290
|
-
const stateFile = modeStatePath(cwd, session, "deep-interview");
|
|
291
|
-
const observed = await readExistingStateForMutation(stateFile);
|
|
292
|
-
if (observed.kind === "absent") throw new Error("DI_STATE_ABSENT");
|
|
293
|
-
if (observed.kind === "corrupt") throw new Error("DI_STATE_CORRUPT");
|
|
294
|
-
const receipt = verifyWorkflowEnvelopeReceiptValue(observed.value, stateFile);
|
|
295
|
-
if (receipt === "receipt-malformed") throw new Error("DI_RECEIPT_MALFORMED");
|
|
296
|
-
if (receipt === "receipt-missing") throw new Error("DI_RECEIPT_MISSING");
|
|
297
|
-
if (receipt === "checksum-mismatch") throw new Error("DI_RECEIPT_CHECKSUM_MISMATCH");
|
|
298
|
-
try {
|
|
299
|
-
if (isNativeDeepInterviewV1(observed.value)) validateDeepInterviewV1Envelope(observed.value);
|
|
300
|
-
} catch {
|
|
301
|
-
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
302
|
-
}
|
|
303
|
-
if (
|
|
304
|
-
observed.value.current_phase === "complete" ||
|
|
305
|
-
observed.value.current_phase === "handoff" ||
|
|
306
|
-
observed.value.active === false
|
|
307
|
-
)
|
|
308
|
-
throw new Error("DI_PHASE_NOT_REPAIRABLE");
|
|
309
|
-
const stateRevision = observed.value.state_revision ?? 0;
|
|
310
|
-
if (!Number.isSafeInteger(stateRevision) || (stateRevision as number) < 0)
|
|
311
|
-
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
312
|
-
const state = asRecord(observed.value.state);
|
|
313
|
-
if (!Object.keys(state).length) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
314
|
-
return { revision: stateRevision as number, state };
|
|
315
|
-
}
|
|
316
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
317
|
-
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
318
|
-
}
|
|
319
|
-
function pointer(pointer: string): string[] {
|
|
320
|
-
if (!pointer.startsWith("/") || pointer.includes("//")) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
321
|
-
const parts = pointer
|
|
322
|
-
.slice(1)
|
|
323
|
-
.split("/")
|
|
324
|
-
.map(part => {
|
|
325
|
-
if (!part || /~(?:[^01]|$)/.test(part)) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
326
|
-
const decoded = part.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
327
|
-
if (FORBIDDEN.has(decoded)) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
328
|
-
return decoded;
|
|
329
|
-
});
|
|
330
|
-
return parts;
|
|
331
|
-
}
|
|
332
|
-
function scaffold(descriptor: DraftDescriptor): Record<string, unknown> | unknown[] {
|
|
333
|
-
if (descriptor.kind === "object") return {};
|
|
334
|
-
if (descriptor.kind === "array") return [];
|
|
335
|
-
throw new Error("DI_DRAFT_INVALID_PATH");
|
|
336
|
-
}
|
|
337
|
-
function child(object: DraftObjectDescriptor, key: string): DraftDescriptor {
|
|
338
|
-
const descriptor = object.fields[key] ?? object.dynamicValue;
|
|
339
|
-
if (!descriptor || (object.dynamicValue && !ID.test(key))) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
340
|
-
return descriptor;
|
|
341
|
-
}
|
|
342
|
-
function index(key: string, length: number): number {
|
|
343
|
-
if (!/^(0|[1-9][0-9]*)$/.test(key)) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
344
|
-
const value = Number(key);
|
|
345
|
-
if (!Number.isSafeInteger(value) || value < 0 || value >= length) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
346
|
-
return value;
|
|
347
|
-
}
|
|
348
|
-
function locate(
|
|
349
|
-
payload: Record<string, unknown>,
|
|
350
|
-
kind: DeepInterviewDraftKind,
|
|
351
|
-
parts: string[],
|
|
352
|
-
): { parent: Record<string, unknown> | unknown[]; key: string; descriptor: DraftDescriptor } {
|
|
353
|
-
let current: Record<string, unknown> | unknown[] = payload;
|
|
354
|
-
let schema: DraftObjectDescriptor | DraftArrayDescriptor = deepInterviewDraftSchema(kind);
|
|
355
|
-
for (let position = 0; position < parts.length - 1; position++) {
|
|
356
|
-
const key = parts[position]!;
|
|
357
|
-
let descriptor: DraftDescriptor;
|
|
358
|
-
if (schema.kind === "object") {
|
|
359
|
-
descriptor = child(schema, key);
|
|
360
|
-
if (descriptor.kind === "leaf") throw new Error("DI_DRAFT_INVALID_PATH");
|
|
361
|
-
const record = current as Record<string, unknown>;
|
|
362
|
-
if (record[key] === undefined) record[key] = scaffold(descriptor);
|
|
363
|
-
if (
|
|
364
|
-
!record[key] ||
|
|
365
|
-
typeof record[key] !== "object" ||
|
|
366
|
-
Array.isArray(record[key]) !== (descriptor.kind === "array")
|
|
367
|
-
)
|
|
368
|
-
throw new Error("DI_DRAFT_INVALID_PATH");
|
|
369
|
-
current = record[key] as Record<string, unknown> | unknown[];
|
|
370
|
-
} else {
|
|
371
|
-
const array = current as unknown[];
|
|
372
|
-
const itemIndex = index(key, array.length);
|
|
373
|
-
descriptor = schema.item;
|
|
374
|
-
if (
|
|
375
|
-
descriptor.kind === "leaf" ||
|
|
376
|
-
!array[itemIndex] ||
|
|
377
|
-
typeof array[itemIndex] !== "object" ||
|
|
378
|
-
Array.isArray(array[itemIndex])
|
|
379
|
-
)
|
|
380
|
-
throw new Error("DI_DRAFT_INVALID_PATH");
|
|
381
|
-
current = array[itemIndex] as Record<string, unknown> | unknown[];
|
|
382
|
-
}
|
|
383
|
-
schema = descriptor as DraftObjectDescriptor | DraftArrayDescriptor;
|
|
384
|
-
}
|
|
385
|
-
const key = parts.at(-1)!;
|
|
386
|
-
const descriptor = schema.kind === "object" ? child(schema, key) : schema.item;
|
|
387
|
-
if (schema.kind === "array") index(key, (current as unknown[]).length);
|
|
388
|
-
return { parent: current, key, descriptor };
|
|
389
|
-
}
|
|
390
|
-
function coerce(value: string, descriptor: DraftLeafDescriptor): Scalar {
|
|
391
|
-
if (descriptor.maxBytes !== undefined && Buffer.byteLength(value) > descriptor.maxBytes)
|
|
392
|
-
throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
393
|
-
switch (descriptor.type) {
|
|
394
|
-
case "string":
|
|
395
|
-
case "text":
|
|
396
|
-
return value;
|
|
397
|
-
case "id":
|
|
398
|
-
if (!ID.test(value)) throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
399
|
-
return value;
|
|
400
|
-
case "enum":
|
|
401
|
-
if (!descriptor.values?.includes(value)) throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
402
|
-
return value;
|
|
403
|
-
case "boolean":
|
|
404
|
-
if (value !== "true" && value !== "false") throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
405
|
-
return value === "true";
|
|
406
|
-
case "number": {
|
|
407
|
-
const parsed = Number(value);
|
|
408
|
-
if (!Number.isFinite(parsed)) throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
409
|
-
return parsed;
|
|
410
|
-
}
|
|
411
|
-
case "safe-int": {
|
|
412
|
-
const parsed = Number(value);
|
|
413
|
-
if (!Number.isSafeInteger(parsed) || Math.abs(parsed) > 10_000) throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
414
|
-
return parsed;
|
|
415
|
-
}
|
|
416
|
-
case "score": {
|
|
417
|
-
const parsed = Number(value);
|
|
418
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed * 10_000) || parsed < 0 || parsed > 1)
|
|
419
|
-
throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
420
|
-
return parsed;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
async function suppliedValue(flags: Map<string, string>, descriptor: DraftLeafDescriptor): Promise<Scalar> {
|
|
425
|
-
const supplied = [flags.has("--value"), flags.has("--value-file"), flags.has("--null")].filter(Boolean).length;
|
|
426
|
-
if (supplied !== 1) throw new Error("DI_INVALID_ARGUMENT");
|
|
427
|
-
if (flags.has("--null")) {
|
|
428
|
-
if (!descriptor.nullable) throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
429
|
-
return null;
|
|
430
|
-
}
|
|
431
|
-
if (flags.has("--value-file")) {
|
|
432
|
-
if (descriptor.type !== "string" && descriptor.type !== "text") throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
433
|
-
const filename = required(flags, "--value-file");
|
|
434
|
-
const metadata = await fs.lstat(filename).catch(() => {
|
|
435
|
-
throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
436
|
-
});
|
|
437
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 4096)
|
|
438
|
-
throw new Error("DI_DRAFT_INVALID_VALUE");
|
|
439
|
-
return coerce(await secureRead(filename, 4096, "DI_DRAFT_INVALID_VALUE"), descriptor);
|
|
440
|
-
}
|
|
441
|
-
return coerce(required(flags, "--value"), descriptor);
|
|
442
|
-
}
|
|
443
|
-
async function editPayload(draft: Draft, flags: Map<string, string>): Promise<void> {
|
|
444
|
-
const op = required(flags, "--op");
|
|
445
|
-
const { parent, key, descriptor } = locate(draft.payload, draft.kind, pointer(required(flags, "--path")));
|
|
446
|
-
const existing = Array.isArray(parent) ? parent[index(key, parent.length)] : parent[key];
|
|
447
|
-
if (op === "remove") {
|
|
448
|
-
if (flags.has("--value") || flags.has("--value-file") || flags.has("--null"))
|
|
449
|
-
throw new Error("DI_INVALID_ARGUMENT");
|
|
450
|
-
if (Array.isArray(parent)) parent.splice(index(key, parent.length), 1);
|
|
451
|
-
else {
|
|
452
|
-
if (!descriptor.optional) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
453
|
-
delete parent[key];
|
|
454
|
-
}
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
if (op === "set") {
|
|
458
|
-
if (descriptor.kind !== "leaf") throw new Error("DI_DRAFT_INVALID_PATH");
|
|
459
|
-
const value = await suppliedValue(flags, descriptor);
|
|
460
|
-
if (Array.isArray(parent)) parent[index(key, parent.length)] = value;
|
|
461
|
-
else parent[key] = value;
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (op !== "append" || flags.has("--null")) throw new Error("DI_INVALID_ARGUMENT");
|
|
465
|
-
if (descriptor.kind !== "array") throw new Error("DI_DRAFT_INVALID_PATH");
|
|
466
|
-
const target = Array.isArray(parent) ? existing : parent[key];
|
|
467
|
-
const output = target === undefined ? [] : target;
|
|
468
|
-
if (!Array.isArray(output) || output.length >= descriptor.maxItems) throw new Error("DI_DRAFT_INVALID_PATH");
|
|
469
|
-
if (descriptor.item.kind === "object") {
|
|
470
|
-
if (flags.has("--value") || flags.has("--value-file")) throw new Error("DI_DRAFT_INVALID_ARGUMENT");
|
|
471
|
-
output.push({});
|
|
472
|
-
} else {
|
|
473
|
-
if (!flags.has("--value") && !flags.has("--value-file") && target === undefined) {
|
|
474
|
-
// A valueless append initializes a missing scalar array without admitting raw JSON.
|
|
475
|
-
if (!Array.isArray(parent)) parent[key] = output;
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
output.push(await suppliedValue(flags, descriptor.item));
|
|
479
|
-
}
|
|
480
|
-
if (!Array.isArray(parent)) parent[key] = output;
|
|
481
|
-
}
|
|
482
|
-
function requestDigest(draft: Draft): string {
|
|
483
|
-
return createHash("sha256")
|
|
484
|
-
.update(
|
|
485
|
-
canonicalDeepInterviewJson({
|
|
486
|
-
kind: draft.kind,
|
|
487
|
-
session_id: draft.session_id,
|
|
488
|
-
round_key: draft.round_key ?? null,
|
|
489
|
-
identity: draft.identity,
|
|
490
|
-
payload: draft.payload,
|
|
491
|
-
}),
|
|
492
|
-
)
|
|
493
|
-
.digest("hex");
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function attemptMatchesDraft(draft: Draft): boolean {
|
|
497
|
-
return draft.attempt?.request_digest === requestDigest(draft);
|
|
498
|
-
}
|
|
499
|
-
function semanticallyEqual(left: unknown, right: unknown): boolean {
|
|
500
|
-
return left === undefined || right === undefined
|
|
501
|
-
? left === right
|
|
502
|
-
: canonicalDeepInterviewJson(left) === canonicalDeepInterviewJson(right);
|
|
503
|
-
}
|
|
504
|
-
function mutationEffectDigest(draft: Draft, state: Record<string, unknown>): string {
|
|
505
|
-
const evidence =
|
|
506
|
-
draft.kind === "initialize-context"
|
|
507
|
-
? Object.fromEntries(
|
|
508
|
-
Object.keys(draft.payload).map(key => [
|
|
509
|
-
key,
|
|
510
|
-
Object.hasOwn(state, key) ? { present: true, value: state[key] } : { present: false },
|
|
511
|
-
]),
|
|
512
|
-
)
|
|
513
|
-
: draft.kind === "confirm-topology"
|
|
514
|
-
? state.topology
|
|
515
|
-
: draft.kind === "record-answer"
|
|
516
|
-
? state.rounds
|
|
517
|
-
: undefined;
|
|
518
|
-
return createHash("sha256")
|
|
519
|
-
.update(canonicalDeepInterviewJson(evidence ?? null))
|
|
520
|
-
.digest("hex");
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function committedDraftMutation(draft: Draft, state: Record<string, unknown>): boolean {
|
|
524
|
-
if (draft.kind === "initialize-context")
|
|
525
|
-
return Object.entries(draft.payload).every(([key, value]) => semanticallyEqual(state[key], value));
|
|
526
|
-
if (draft.kind === "confirm-topology") {
|
|
527
|
-
const topology = asRecord(state.topology);
|
|
528
|
-
const { status: _status, confirmed_at: _confirmedAt, ...value } = topology;
|
|
529
|
-
return (
|
|
530
|
-
topology.status === "confirmed" &&
|
|
531
|
-
canonicalDeepInterviewJson(value) === canonicalDeepInterviewJson(draft.payload)
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
if (draft.kind === "record-answer") {
|
|
535
|
-
return (Array.isArray(state.rounds) ? state.rounds : [])
|
|
536
|
-
.map(asRecord)
|
|
537
|
-
.some(
|
|
538
|
-
round =>
|
|
539
|
-
round.round === Number(draft.identity.round) &&
|
|
540
|
-
round.question_id === draft.identity["question-id"] &&
|
|
541
|
-
round.round_id === (draft.identity["round-id"] || undefined) &&
|
|
542
|
-
round.component === (draft.identity["component-id"] || undefined) &&
|
|
543
|
-
round.dimension === (draft.identity.dimension || undefined) &&
|
|
544
|
-
round.question_text === draft.payload.question &&
|
|
545
|
-
semanticallyEqual(round.selected_options, asRecord(draft.payload.answer).selected_options) &&
|
|
546
|
-
round.custom_input === asRecord(draft.payload.answer).custom_input &&
|
|
547
|
-
round.lifecycle === "answered",
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
async function injectCompetingWrite(cwd: string, draft: Draft, revision: number): Promise<void> {
|
|
554
|
-
if (process.env.GJC_DEEP_INTERVIEW_DRAFT_INJECT_COMPETING_WRITE !== "1") return;
|
|
555
|
-
await transformGuardedWorkflowEnvelopeAtomic(modeStatePath(cwd, draft.session_id, "deep-interview"), {
|
|
556
|
-
cwd,
|
|
557
|
-
expectedRevision: revision,
|
|
558
|
-
receipt: {
|
|
559
|
-
cwd,
|
|
560
|
-
skill: "deep-interview",
|
|
561
|
-
owner: "gjc-runtime",
|
|
562
|
-
command: "gjc deep-interview test competing write",
|
|
563
|
-
sessionId: draft.session_id,
|
|
564
|
-
nowIso: new Date().toISOString(),
|
|
565
|
-
},
|
|
566
|
-
transform(current) {
|
|
567
|
-
const state = asRecord(current.state);
|
|
568
|
-
return { kind: "write", value: { ...current, state: { ...state, injected_competing_write: true } } };
|
|
569
|
-
},
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
async function runDeepInterviewDraftCommandInternal(
|
|
574
|
-
input: readonly string[],
|
|
575
|
-
cwd: string,
|
|
576
|
-
allowInternalConsume: boolean,
|
|
577
|
-
): Promise<DeepInterviewDraftResult> {
|
|
578
|
-
try {
|
|
579
|
-
const action = input[0];
|
|
580
|
-
if (
|
|
581
|
-
!action ||
|
|
582
|
-
![
|
|
583
|
-
...["create", "edit", "show", "check", "rebase", "discard"],
|
|
584
|
-
...(allowInternalConsume ? ["consume-internal"] : []),
|
|
585
|
-
].includes(action)
|
|
586
|
-
)
|
|
587
|
-
throw new Error("DI_UNKNOWN_COMMAND");
|
|
588
|
-
const flags = args(input);
|
|
589
|
-
const allowed: Record<string, readonly string[]> = {
|
|
590
|
-
create: [
|
|
591
|
-
"--for",
|
|
592
|
-
"--kind",
|
|
593
|
-
"--session-id",
|
|
594
|
-
"--round-key",
|
|
595
|
-
"--round",
|
|
596
|
-
"--question-id",
|
|
597
|
-
"--round-id",
|
|
598
|
-
"--component-id",
|
|
599
|
-
"--dimension",
|
|
600
|
-
],
|
|
601
|
-
edit: ["--draft-id", "--expected-draft-revision", "--op", "--path", "--value", "--value-file", "--null"],
|
|
602
|
-
show: ["--draft-id"],
|
|
603
|
-
check: ["--draft-id"],
|
|
604
|
-
rebase: ["--draft-id", "--expected-draft-revision", "--to-state-revision"],
|
|
605
|
-
discard: ["--draft-id", "--expected-draft-revision"],
|
|
606
|
-
"consume-internal": ["--draft-id", "--expected-draft-revision", "--kind"],
|
|
607
|
-
};
|
|
608
|
-
allow(flags, [...allowed[action], "--json"]);
|
|
609
|
-
if (action !== "consume-internal" && !flags.has("--json")) throw new Error("DI_INVALID_ARGUMENT");
|
|
610
|
-
return await withLock(cwd, async () => {
|
|
611
|
-
if (action === "create") await cleanup(cwd);
|
|
612
|
-
if (action === "create") {
|
|
613
|
-
const suppliedKinds = [flags.has("--for"), flags.has("--kind")].filter(Boolean).length;
|
|
614
|
-
if (suppliedKinds !== 1) throw new Error("DI_INVALID_ARGUMENT");
|
|
615
|
-
const kind = (flags.get("--for") ?? flags.get("--kind")) as DeepInterviewDraftKind;
|
|
616
|
-
const session = required(flags, "--session-id");
|
|
617
|
-
if (!DEEP_INTERVIEW_DRAFT_KINDS.includes(kind) || !ID.test(session)) throw new Error("DI_INVALID_ARGUMENT");
|
|
618
|
-
const current = await revision(cwd, session);
|
|
619
|
-
const now = new Date();
|
|
620
|
-
const draft: Draft = {
|
|
621
|
-
version: 1,
|
|
622
|
-
id: randomBytes(16).toString("hex"),
|
|
623
|
-
workspace: path.resolve(cwd),
|
|
624
|
-
session_id: session,
|
|
625
|
-
kind,
|
|
626
|
-
base_revision: current.revision,
|
|
627
|
-
draft_revision: 1,
|
|
628
|
-
created_at: now.toISOString(),
|
|
629
|
-
updated_at: now.toISOString(),
|
|
630
|
-
expires_at: new Date(now.getTime() + ACTIVE_MS).toISOString(),
|
|
631
|
-
status: "active",
|
|
632
|
-
round_key: flags.get("--round-key"),
|
|
633
|
-
identity: Object.fromEntries(
|
|
634
|
-
["--round", "--question-id", "--round-id", "--component-id", "--dimension"].flatMap(key =>
|
|
635
|
-
flags.has(key) ? [[key.slice(2), flags.get(key)!]] : [],
|
|
636
|
-
),
|
|
637
|
-
),
|
|
638
|
-
payload: {},
|
|
639
|
-
};
|
|
640
|
-
if (kind === "record-answer" && (!draft.identity.round || !draft.identity["question-id"]))
|
|
641
|
-
throw new Error("DI_INVALID_ARGUMENT");
|
|
642
|
-
if (kind === "apply-round-result") {
|
|
643
|
-
const shells = (Array.isArray(current.state.rounds) ? current.state.rounds : [])
|
|
644
|
-
.map(asRecord)
|
|
645
|
-
.filter(item => item.lifecycle === "answered");
|
|
646
|
-
if (!draft.round_key && shells.length === 0) throw new Error("DI_PENDING_SHELL_NOT_FOUND");
|
|
647
|
-
if (!draft.round_key && shells.length !== 1) throw new Error("DI_PENDING_SHELL_AMBIGUOUS");
|
|
648
|
-
const shell = draft.round_key ? shells.find(item => item.round_key === draft.round_key) : shells[0];
|
|
649
|
-
if (!shell) throw new Error("DI_PENDING_SHELL_NOT_FOUND");
|
|
650
|
-
draft.round_key = String(shell.round_key);
|
|
651
|
-
draft.identity = {
|
|
652
|
-
round: String(shell.round),
|
|
653
|
-
"question-id": String(shell.question_id),
|
|
654
|
-
"round-id": String(shell.round_id ?? ""),
|
|
655
|
-
"component-id": String(shell.component ?? ""),
|
|
656
|
-
dimension: String(shell.dimension ?? ""),
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
await write(cwd, draft);
|
|
660
|
-
return response(0, { ok: true, draft });
|
|
661
|
-
}
|
|
662
|
-
const id = required(flags, "--draft-id");
|
|
663
|
-
const draft = await read(cwd, id);
|
|
664
|
-
await cleanup(cwd);
|
|
665
|
-
if (action === "show") return response(0, { ok: true, draft });
|
|
666
|
-
if (action === "discard") {
|
|
667
|
-
if (Number(required(flags, "--expected-draft-revision")) !== draft.draft_revision)
|
|
668
|
-
throw new Error("DI_DRAFT_REVISION_CONFLICT");
|
|
669
|
-
await fs.unlink(await file(cwd, id));
|
|
670
|
-
return response(0, { ok: true, draft_id: id, discarded: true });
|
|
671
|
-
}
|
|
672
|
-
if (action === "edit") {
|
|
673
|
-
if (
|
|
674
|
-
draft.status !== "active" ||
|
|
675
|
-
Number(required(flags, "--expected-draft-revision")) !== draft.draft_revision
|
|
676
|
-
)
|
|
677
|
-
throw new Error("DI_DRAFT_REVISION_CONFLICT");
|
|
678
|
-
await editPayload(draft, flags);
|
|
679
|
-
draft.draft_revision++;
|
|
680
|
-
draft.updated_at = new Date().toISOString();
|
|
681
|
-
await write(cwd, draft);
|
|
682
|
-
return response(0, { ok: true, draft });
|
|
683
|
-
}
|
|
684
|
-
if (action === "rebase") {
|
|
685
|
-
if (
|
|
686
|
-
draft.status !== "active" ||
|
|
687
|
-
Number(required(flags, "--expected-draft-revision")) !== draft.draft_revision
|
|
688
|
-
)
|
|
689
|
-
throw new Error("DI_DRAFT_REVISION_CONFLICT");
|
|
690
|
-
const current = await revision(cwd, draft.session_id);
|
|
691
|
-
if (Number(required(flags, "--to-state-revision")) !== current.revision)
|
|
692
|
-
throw new Error("DI_STATE_REVISION_CONFLICT");
|
|
693
|
-
validateDraftPayload(draft.kind, draft.payload, current.state);
|
|
694
|
-
draft.base_revision = current.revision;
|
|
695
|
-
draft.attempt = undefined;
|
|
696
|
-
draft.draft_revision++;
|
|
697
|
-
draft.updated_at = new Date().toISOString();
|
|
698
|
-
await write(cwd, draft);
|
|
699
|
-
return response(0, { ok: true, draft });
|
|
700
|
-
}
|
|
701
|
-
if (action === "consume-internal") {
|
|
702
|
-
if (flags.get("--kind") !== draft.kind) throw new Error("DI_INPUT_MODE_CONFLICT");
|
|
703
|
-
if (Number(required(flags, "--expected-draft-revision")) !== draft.draft_revision)
|
|
704
|
-
throw new Error("DI_DRAFT_REVISION_CONFLICT");
|
|
705
|
-
}
|
|
706
|
-
if (draft.status === "consumed")
|
|
707
|
-
return response(0, { ok: true, draft_id: id, consumed: true, receipt: draft.receipt });
|
|
708
|
-
const current = await revision(cwd, draft.session_id);
|
|
709
|
-
let schemaIssue: string | undefined;
|
|
710
|
-
try {
|
|
711
|
-
validateDraftPayload(draft.kind, draft.payload, current.state);
|
|
712
|
-
} catch (error) {
|
|
713
|
-
schemaIssue = code(error);
|
|
714
|
-
}
|
|
715
|
-
if (action === "check")
|
|
716
|
-
return response(0, {
|
|
717
|
-
ok: true,
|
|
718
|
-
draft_id: id,
|
|
719
|
-
valid: schemaIssue === undefined,
|
|
720
|
-
state_revision: current.revision,
|
|
721
|
-
stale: draft.base_revision !== current.revision,
|
|
722
|
-
issues: schemaIssue ? [{ code: schemaIssue, message: schemaIssue }] : [],
|
|
723
|
-
});
|
|
724
|
-
if (schemaIssue) throw new Error(schemaIssue);
|
|
725
|
-
const legacy = [
|
|
726
|
-
draft.kind,
|
|
727
|
-
"--json",
|
|
728
|
-
"--session-id",
|
|
729
|
-
draft.session_id,
|
|
730
|
-
"--schema-version",
|
|
731
|
-
"1",
|
|
732
|
-
"--expected-revision",
|
|
733
|
-
String(draft.attempt ? current.revision : draft.base_revision),
|
|
734
|
-
];
|
|
735
|
-
if (draft.kind === "initialize-context" || draft.kind === "confirm-topology")
|
|
736
|
-
legacy.push("--input-json", JSON.stringify(draft.payload));
|
|
737
|
-
if (draft.kind === "record-answer")
|
|
738
|
-
legacy.push(
|
|
739
|
-
"--round",
|
|
740
|
-
required(new Map(Object.entries(draft.identity)), "round"),
|
|
741
|
-
"--question-id",
|
|
742
|
-
required(new Map(Object.entries(draft.identity)), "question-id"),
|
|
743
|
-
"--question-json",
|
|
744
|
-
String(draft.payload.question),
|
|
745
|
-
"--answer-json",
|
|
746
|
-
JSON.stringify(draft.payload.answer),
|
|
747
|
-
);
|
|
748
|
-
if (draft.kind === "record-answer")
|
|
749
|
-
for (const key of ["round-id", "component-id", "dimension"]) {
|
|
750
|
-
const value = draft.identity[key];
|
|
751
|
-
if (value) legacy.push(`--${key}`, value);
|
|
752
|
-
}
|
|
753
|
-
if (draft.kind === "apply-round-result") {
|
|
754
|
-
const shell = (Array.isArray(current.state.rounds) ? current.state.rounds : [])
|
|
755
|
-
.map(asRecord)
|
|
756
|
-
.find(round => round.round_key === draft.round_key);
|
|
757
|
-
if (!shell) throw new Error("DI_PENDING_SHELL_NOT_FOUND");
|
|
758
|
-
legacy.push(
|
|
759
|
-
"--round",
|
|
760
|
-
String(shell.round),
|
|
761
|
-
"--question-id",
|
|
762
|
-
String(shell.question_id),
|
|
763
|
-
"--result-json",
|
|
764
|
-
JSON.stringify({
|
|
765
|
-
...draft.payload,
|
|
766
|
-
...(draft.payload.bookkeeping
|
|
767
|
-
? {
|
|
768
|
-
bookkeeping: {
|
|
769
|
-
...asRecord(draft.payload.bookkeeping),
|
|
770
|
-
resolution:
|
|
771
|
-
asRecord(draft.payload.bookkeeping).resolution === "complete"
|
|
772
|
-
? "direct"
|
|
773
|
-
: asRecord(draft.payload.bookkeeping).resolution,
|
|
774
|
-
},
|
|
775
|
-
}
|
|
776
|
-
: {}),
|
|
777
|
-
}),
|
|
778
|
-
);
|
|
779
|
-
if (typeof shell.round_id === "string") legacy.push("--round-id", shell.round_id);
|
|
780
|
-
}
|
|
781
|
-
const replayingAttempt = draft.attempt !== undefined;
|
|
782
|
-
if (!draft.attempt) {
|
|
783
|
-
draft.attempt = {
|
|
784
|
-
state_revision: current.revision,
|
|
785
|
-
request_digest: requestDigest(draft),
|
|
786
|
-
effect_digest: mutationEffectDigest(draft, current.state),
|
|
787
|
-
};
|
|
788
|
-
draft.updated_at = new Date().toISOString();
|
|
789
|
-
await write(cwd, draft);
|
|
790
|
-
await injectCompetingWrite(cwd, draft, current.revision);
|
|
791
|
-
} else if (
|
|
792
|
-
current.revision !== draft.attempt.state_revision + 1 ||
|
|
793
|
-
!attemptMatchesDraft(draft) ||
|
|
794
|
-
draft.attempt.effect_digest === mutationEffectDigest(draft, current.state) ||
|
|
795
|
-
!committedDraftMutation(draft, current.state)
|
|
796
|
-
) {
|
|
797
|
-
throw new Error("DI_STATE_REVISION_CONFLICT");
|
|
798
|
-
}
|
|
799
|
-
const result: DeepInterviewRepairResult = await runDeepInterviewRepairCommand(legacy, cwd);
|
|
800
|
-
if (result.status !== 0) {
|
|
801
|
-
if (result.stderr?.includes("DI_REVISION_CONFLICT")) throw new Error("DI_STATE_REVISION_CONFLICT");
|
|
802
|
-
return result;
|
|
803
|
-
}
|
|
804
|
-
draft.status = "consumed";
|
|
805
|
-
draft.updated_at = new Date().toISOString();
|
|
806
|
-
draft.receipt = JSON.parse(result.stdout ?? "{}") as Record<string, unknown>;
|
|
807
|
-
draft.attempt = undefined;
|
|
808
|
-
if (process.env.GJC_DEEP_INTERVIEW_DRAFT_FAIL_RECEIPT_PERSISTENCE === "1" && !replayingAttempt)
|
|
809
|
-
throw new Error("DI_DRAFT_RECEIPT_PERSIST_FAILED");
|
|
810
|
-
await write(cwd, draft);
|
|
811
|
-
return response(0, { ok: true, draft_id: id, consumed: true, receipt: draft.receipt });
|
|
812
|
-
});
|
|
813
|
-
} catch (error) {
|
|
814
|
-
return response(code(error) === "DI_STATE_REVISION_CONFLICT" ? 3 : 2, code(error));
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
export async function runDeepInterviewDraftCommand(
|
|
818
|
-
input: readonly string[],
|
|
819
|
-
cwd: string,
|
|
820
|
-
): Promise<DeepInterviewDraftResult> {
|
|
821
|
-
return runDeepInterviewDraftCommandInternal(input, cwd, false);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
export async function runDeepInterviewDraftInternalConsumeCommand(
|
|
825
|
-
draftId: string,
|
|
826
|
-
expectedDraftRevision: number,
|
|
827
|
-
kind: DeepInterviewDraftKind,
|
|
828
|
-
cwd: string,
|
|
829
|
-
): Promise<DeepInterviewDraftResult> {
|
|
830
|
-
return runDeepInterviewDraftCommandInternal(
|
|
831
|
-
[
|
|
832
|
-
"consume-internal",
|
|
833
|
-
"--draft-id",
|
|
834
|
-
draftId,
|
|
835
|
-
"--expected-draft-revision",
|
|
836
|
-
String(expectedDraftRevision),
|
|
837
|
-
"--kind",
|
|
838
|
-
kind,
|
|
839
|
-
],
|
|
840
|
-
cwd,
|
|
841
|
-
true,
|
|
842
|
-
);
|
|
843
|
-
}
|