@gajae-code/coding-agent 0.8.1 → 0.9.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 +63 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +105 -1
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +28 -1
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config.d.ts +14 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +23 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/notifications/threaded-render.d.ts +6 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +53 -1
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +7 -7
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +91 -7
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings-schema.ts +68 -1
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
- package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
- package/src/defaults/gjc/skills/team/SKILL.md +17 -14
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +44 -2
- package/src/gjc-runtime/session-state-sidecar.ts +263 -20
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +187 -15
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1663 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +16 -7
- package/src/main.ts +95 -43
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +180 -18
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +51 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/rpc/rpc-mode.ts +29 -3
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/notifications/config.ts +25 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +117 -14
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-cli.ts +2 -2
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +302 -51
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/notifications/threaded-render.ts +17 -1
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +29 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/computer.md +2 -2
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +1 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +52 -28
- package/src/session/agent-session.ts +951 -132
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +218 -71
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/computer.ts +246 -43
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/tools/telegram-send.ts +6 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/web/scrapers/youtube.ts +24 -12
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -1,14 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
1
3
|
import * as path from "node:path";
|
|
2
4
|
import { loadCustomTools } from "../custom-tools/loader";
|
|
3
5
|
import type { CustomTool } from "../custom-tools/types";
|
|
4
|
-
import { loadEffectiveGjcPluginRegistry } from "./registry";
|
|
5
|
-
import { type SessionQuarantine,
|
|
6
|
+
import { loadEffectiveGjcPluginRegistry, registryPathForScope } from "./registry";
|
|
7
|
+
import { type SessionQuarantine, type SessionValidationResult, validateSessionBundles } from "./session-validation";
|
|
8
|
+
import type { GjcPluginRegistryEntry, GjcPluginScope } from "./types";
|
|
6
9
|
|
|
7
10
|
export interface AlwaysOnPluginTools {
|
|
8
11
|
tools: CustomTool[];
|
|
9
12
|
quarantine: SessionQuarantine[];
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
interface FileSnapshot {
|
|
16
|
+
path: string;
|
|
17
|
+
mtimeMs: number;
|
|
18
|
+
ctimeMs: number;
|
|
19
|
+
size: number;
|
|
20
|
+
ino: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ValidatedPluginRegistry {
|
|
24
|
+
effective: GjcPluginRegistryEntry[];
|
|
25
|
+
active: GjcPluginRegistryEntry[];
|
|
26
|
+
quarantine: SessionQuarantine[];
|
|
27
|
+
validation: SessionValidationResult;
|
|
28
|
+
registryFiles: FileSnapshot[];
|
|
29
|
+
pluginFiles: FileSnapshot[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface CachedValidatedPluginRegistry extends ValidatedPluginRegistry {
|
|
33
|
+
registryKey: string;
|
|
34
|
+
pluginKey: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const validatedRegistryCache = new Map<string, CachedValidatedPluginRegistry>();
|
|
38
|
+
const hashCache = new Map<string, string>();
|
|
39
|
+
// Bound the digest memo so long sessions with plugin churn cannot grow it
|
|
40
|
+
// unboundedly; entries are re-derivable from disk at the cost of one read.
|
|
41
|
+
const HASH_CACHE_MAX_ENTRIES = 512;
|
|
42
|
+
const registryScopes: GjcPluginScope[] = ["user", "project"];
|
|
43
|
+
|
|
44
|
+
async function snapshotExistingFile(filePath: string): Promise<FileSnapshot | null> {
|
|
45
|
+
try {
|
|
46
|
+
const stat = await fs.stat(filePath);
|
|
47
|
+
return { path: filePath, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, size: stat.size, ino: stat.ino };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return null;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function snapshotsKey(snapshots: readonly FileSnapshot[]): string {
|
|
55
|
+
return snapshots.map(s => `${s.path}:${s.mtimeMs}:${s.ctimeMs}:${s.size}:${s.ino}`).join("|");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function snapshotRegistryFiles(cwd: string): Promise<FileSnapshot[]> {
|
|
59
|
+
const snapshots = await Promise.all(
|
|
60
|
+
registryScopes.map(scope => snapshotExistingFile(registryPathForScope(scope, cwd))),
|
|
61
|
+
);
|
|
62
|
+
return snapshots.filter((s): s is FileSnapshot => s !== null);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function snapshotPluginFiles(entries: readonly GjcPluginRegistryEntry[]): Promise<FileSnapshot[]> {
|
|
66
|
+
const snapshots: FileSnapshot[] = [];
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!entry.enabled) continue;
|
|
69
|
+
for (const file of entry.copiedFiles) {
|
|
70
|
+
const abs = path.join(entry.pluginRoot, file.relativePath);
|
|
71
|
+
const snapshot = await snapshotExistingFile(abs);
|
|
72
|
+
if (!snapshot) {
|
|
73
|
+
snapshots.push({ path: abs, mtimeMs: Number.NaN, ctimeMs: Number.NaN, size: Number.NaN, ino: Number.NaN });
|
|
74
|
+
} else {
|
|
75
|
+
snapshots.push(snapshot);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return snapshots;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sha256(buf: Buffer): string {
|
|
83
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function hashFile(snapshot: FileSnapshot): Promise<string> {
|
|
87
|
+
const key = `${snapshot.path}:${snapshot.mtimeMs}:${snapshot.ctimeMs}:${snapshot.size}:${snapshot.ino}`;
|
|
88
|
+
const cached = hashCache.get(key);
|
|
89
|
+
if (cached) return cached;
|
|
90
|
+
const digest = sha256(await fs.readFile(snapshot.path));
|
|
91
|
+
if (hashCache.size >= HASH_CACHE_MAX_ENTRIES) {
|
|
92
|
+
// FIFO eviction is sufficient: the memo only avoids re-reads within a
|
|
93
|
+
// session; correctness never depends on a hit.
|
|
94
|
+
const oldest = hashCache.keys().next().value;
|
|
95
|
+
if (oldest !== undefined) hashCache.delete(oldest);
|
|
96
|
+
}
|
|
97
|
+
hashCache.set(key, digest);
|
|
98
|
+
return digest;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function verifyEntryHashesCached(entry: GjcPluginRegistryEntry): Promise<SessionQuarantine | null> {
|
|
102
|
+
for (const file of entry.copiedFiles) {
|
|
103
|
+
const abs = path.join(entry.pluginRoot, file.relativePath);
|
|
104
|
+
const snapshot = await snapshotExistingFile(abs);
|
|
105
|
+
if (!snapshot) {
|
|
106
|
+
return {
|
|
107
|
+
plugin: entry.name,
|
|
108
|
+
surfaceId: `plugin:${entry.name}`,
|
|
109
|
+
code: "runtime_mismatch",
|
|
110
|
+
message: `Installed file missing: ${file.relativePath}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if ((await hashFile(snapshot)) !== file.sha256) {
|
|
114
|
+
return {
|
|
115
|
+
plugin: entry.name,
|
|
116
|
+
surfaceId: `plugin:${entry.name}`,
|
|
117
|
+
code: "runtime_mismatch",
|
|
118
|
+
message: `Installed file hash drift: ${file.relativePath}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function loadValidatedPluginRegistry(cwd: string): Promise<ValidatedPluginRegistry> {
|
|
126
|
+
const registryFiles = await snapshotRegistryFiles(cwd);
|
|
127
|
+
const registryKey = snapshotsKey(registryFiles);
|
|
128
|
+
const cached = validatedRegistryCache.get(cwd);
|
|
129
|
+
if (cached && cached.registryKey === registryKey) {
|
|
130
|
+
const pluginFiles = await snapshotPluginFiles(cached.effective);
|
|
131
|
+
const pluginKey = snapshotsKey(pluginFiles);
|
|
132
|
+
if (cached.pluginKey === pluginKey) return cached;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const effective = await loadEffectiveGjcPluginRegistry(cwd);
|
|
136
|
+
const currentRegistryFiles = await snapshotRegistryFiles(cwd);
|
|
137
|
+
const preQuarantine: SessionQuarantine[] = [];
|
|
138
|
+
for (const entry of effective) {
|
|
139
|
+
if (!entry.enabled) continue;
|
|
140
|
+
const drift = await verifyEntryHashesCached(entry);
|
|
141
|
+
if (drift) preQuarantine.push(drift);
|
|
142
|
+
}
|
|
143
|
+
const validation = validateSessionBundles(effective, {}, preQuarantine);
|
|
144
|
+
const pluginFiles = await snapshotPluginFiles(effective);
|
|
145
|
+
const next: CachedValidatedPluginRegistry = {
|
|
146
|
+
effective,
|
|
147
|
+
active: validation.active,
|
|
148
|
+
quarantine: validation.quarantine,
|
|
149
|
+
validation,
|
|
150
|
+
registryFiles: currentRegistryFiles,
|
|
151
|
+
pluginFiles,
|
|
152
|
+
registryKey: snapshotsKey(currentRegistryFiles),
|
|
153
|
+
pluginKey: snapshotsKey(pluginFiles),
|
|
154
|
+
};
|
|
155
|
+
validatedRegistryCache.set(cwd, next);
|
|
156
|
+
return next;
|
|
157
|
+
}
|
|
158
|
+
|
|
12
159
|
/**
|
|
13
160
|
* Load the always-on plugin tool surfaces for the effective registry at `cwd`.
|
|
14
161
|
*
|
|
@@ -26,22 +173,15 @@ export async function loadAlwaysOnPluginTools(input: {
|
|
|
26
173
|
cwd: string;
|
|
27
174
|
reservedToolNames: string[];
|
|
28
175
|
}): Promise<AlwaysOnPluginTools> {
|
|
29
|
-
const
|
|
176
|
+
const validated = await loadValidatedPluginRegistry(input.cwd);
|
|
177
|
+
const { effective } = validated;
|
|
30
178
|
if (effective.length === 0) return { tools: [], quarantine: [] };
|
|
31
179
|
|
|
32
|
-
// Hash-drift quarantine before importing any plugin code.
|
|
33
|
-
const preQuarantine: SessionQuarantine[] = [];
|
|
34
|
-
for (const entry of effective) {
|
|
35
|
-
if (!entry.enabled) continue;
|
|
36
|
-
const drift = await verifyEntryHashes(entry);
|
|
37
|
-
if (drift) preQuarantine.push(drift);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
180
|
const reserved = new Set(input.reservedToolNames);
|
|
41
181
|
const { active, quarantine } = validateSessionBundles(
|
|
42
182
|
effective,
|
|
43
183
|
{ toolNames: input.reservedToolNames },
|
|
44
|
-
|
|
184
|
+
validated.quarantine,
|
|
45
185
|
);
|
|
46
186
|
|
|
47
187
|
// Map declared (path -> name) for every active always-on tool surface.
|
|
@@ -109,15 +249,8 @@ export async function loadAlwaysOnPluginTools(input: {
|
|
|
109
249
|
* plugins are installed/enabled. Safe to call unconditionally at session start.
|
|
110
250
|
*/
|
|
111
251
|
export async function renderAlwaysOnSystemAppendices(input: { cwd: string }): Promise<string> {
|
|
112
|
-
const effective = await
|
|
252
|
+
const { effective, active } = await loadValidatedPluginRegistry(input.cwd);
|
|
113
253
|
if (effective.length === 0) return "";
|
|
114
|
-
const preQuarantine: SessionQuarantine[] = [];
|
|
115
|
-
for (const entry of effective) {
|
|
116
|
-
if (!entry.enabled) continue;
|
|
117
|
-
const drift = await verifyEntryHashes(entry);
|
|
118
|
-
if (drift) preQuarantine.push(drift);
|
|
119
|
-
}
|
|
120
|
-
const { active } = validateSessionBundles(effective, {}, preQuarantine);
|
|
121
254
|
const { renderPluginAppendices } = await import("./prompt-appendix");
|
|
122
255
|
return (await renderPluginAppendices(active)).system;
|
|
123
256
|
}
|
|
@@ -131,15 +264,8 @@ export async function renderAgentPromptAdditions(input: {
|
|
|
131
264
|
cwd: string;
|
|
132
265
|
agentName: string;
|
|
133
266
|
}): Promise<{ appendix: string; advertisement: string }> {
|
|
134
|
-
const effective = await
|
|
267
|
+
const { effective, active } = await loadValidatedPluginRegistry(input.cwd);
|
|
135
268
|
if (effective.length === 0) return { appendix: "", advertisement: "" };
|
|
136
|
-
const preQuarantine: SessionQuarantine[] = [];
|
|
137
|
-
for (const entry of effective) {
|
|
138
|
-
if (!entry.enabled) continue;
|
|
139
|
-
const drift = await verifyEntryHashes(entry);
|
|
140
|
-
if (drift) preQuarantine.push(drift);
|
|
141
|
-
}
|
|
142
|
-
const { active } = validateSessionBundles(effective, {}, preQuarantine);
|
|
143
269
|
const { renderPluginAppendices } = await import("./prompt-appendix");
|
|
144
270
|
const { buildAgentSubskillAdvertisement } = await import("./injection");
|
|
145
271
|
const rendered = await renderPluginAppendices(active);
|
|
@@ -158,15 +284,8 @@ export async function renderSkillAdvertisement(input: {
|
|
|
158
284
|
skillName: string;
|
|
159
285
|
phase?: string;
|
|
160
286
|
}): Promise<string> {
|
|
161
|
-
const effective = await
|
|
287
|
+
const { effective, active } = await loadValidatedPluginRegistry(input.cwd);
|
|
162
288
|
if (effective.length === 0) return "";
|
|
163
|
-
const preQuarantine: SessionQuarantine[] = [];
|
|
164
|
-
for (const entry of effective) {
|
|
165
|
-
if (!entry.enabled) continue;
|
|
166
|
-
const drift = await verifyEntryHashes(entry);
|
|
167
|
-
if (drift) preQuarantine.push(drift);
|
|
168
|
-
}
|
|
169
|
-
const { active } = validateSessionBundles(effective, {}, preQuarantine);
|
|
170
289
|
const { buildSubskillAdvertisement } = await import("./injection");
|
|
171
290
|
return buildSubskillAdvertisement(active, input.skillName, input.phase);
|
|
172
291
|
}
|
|
@@ -181,15 +300,8 @@ export async function buildPluginMcpConfigs(input: { cwd: string }): Promise<{
|
|
|
181
300
|
configs: Record<string, any>;
|
|
182
301
|
quarantine: SessionQuarantine[];
|
|
183
302
|
}> {
|
|
184
|
-
const effective = await
|
|
303
|
+
const { effective, active, quarantine } = await loadValidatedPluginRegistry(input.cwd);
|
|
185
304
|
if (effective.length === 0) return { configs: {}, quarantine: [] };
|
|
186
|
-
const preQuarantine: SessionQuarantine[] = [];
|
|
187
|
-
for (const entry of effective) {
|
|
188
|
-
if (!entry.enabled) continue;
|
|
189
|
-
const drift = await verifyEntryHashes(entry);
|
|
190
|
-
if (drift) preQuarantine.push(drift);
|
|
191
|
-
}
|
|
192
|
-
const { active, quarantine } = validateSessionBundles(effective, {}, preQuarantine);
|
|
193
305
|
const { assertMcpInstallPolicy, assertDnsResolvesPublic, assertUrlAllowed } = await import("./mcp-policy");
|
|
194
306
|
const nodePath = await import("node:path");
|
|
195
307
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DeepInterviewEstablishedFact,
|
|
3
|
+
type DeepInterviewRoundRecord,
|
|
4
|
+
type DeepInterviewStateEnvelope,
|
|
5
|
+
normalizeDeepInterviewEnvelope,
|
|
6
|
+
} from "./deep-interview-state";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic ambiguity floor for deep-interview.
|
|
10
|
+
*
|
|
11
|
+
* The interview's ambiguity score is reported by an LLM scorer, which anchors on
|
|
12
|
+
* prior scores and under-reports rises when the user pivots or contradicts earlier
|
|
13
|
+
* answers. Following the Ouroboros `max(llm_score, deterministic_floor(ledger))`
|
|
14
|
+
* principle, this pure leaf module computes a code-level lower bound from evidence
|
|
15
|
+
* already persisted in deep-interview state, so the reported score can never fall
|
|
16
|
+
* below what code can objectively measure:
|
|
17
|
+
*
|
|
18
|
+
* - `0.10` per established fact marked disputed with no `superseded_by` resolution
|
|
19
|
+
* (contradiction pressure — a pivot keeps ambiguity elevated until resolved);
|
|
20
|
+
* - `0.05` per active topology component whose goal/constraints/criteria clarity
|
|
21
|
+
* is still unscored (gap pressure — a sibling component cannot hide);
|
|
22
|
+
* - `0.05 × (auto-answered rounds / scored rounds)` (assumption dilution).
|
|
23
|
+
*
|
|
24
|
+
* Like `deep-interview-state`, this module MUST stay pure and dependency-free
|
|
25
|
+
* (no filesystem, no state-writer, no CLI runtime) so every writer can apply it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Weights (mirrors the Ouroboros deterministic_floor coefficients)
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
const DISPUTED_FACT_WEIGHT = 0.1;
|
|
33
|
+
const UNSCORED_COMPONENT_WEIGHT = 0.05;
|
|
34
|
+
const AUTO_ANSWER_DILUTION_WEIGHT = 0.05;
|
|
35
|
+
|
|
36
|
+
const CORE_CLARITY_DIMENSIONS = ["goal", "constraints", "criteria"] as const;
|
|
37
|
+
|
|
38
|
+
export interface AmbiguityFloorBreakdown {
|
|
39
|
+
floor: number;
|
|
40
|
+
disputed_fact_count: number;
|
|
41
|
+
unscored_active_component_count: number;
|
|
42
|
+
auto_answer_ratio: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AmbiguityClampResult {
|
|
46
|
+
effective: number;
|
|
47
|
+
clamped: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Pure helpers
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asArray(value: unknown): unknown[] {
|
|
59
|
+
return Array.isArray(value) ? value : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function round2(value: number): number {
|
|
63
|
+
return Math.round(value * 100) / 100;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A disputed fact contributes pressure until it is resolved via `superseded_by` or re-confirmation. */
|
|
67
|
+
function isUnresolvedDisputedFact(value: unknown): boolean {
|
|
68
|
+
if (!isPlainObject(value)) return false;
|
|
69
|
+
const fact = value as Partial<DeepInterviewEstablishedFact>;
|
|
70
|
+
if (fact.disputed !== true) return false;
|
|
71
|
+
return typeof fact.superseded_by !== "string" || fact.superseded_by.trim() === "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An active component is unscored while any core clarity dimension lacks a finite
|
|
76
|
+
* numeric score. Deferred components are excluded (they are outside ambiguity math),
|
|
77
|
+
* and the gate only arms once the topology was explicitly confirmed in Round 0.
|
|
78
|
+
*/
|
|
79
|
+
function countUnscoredActiveComponents(topology: unknown): number {
|
|
80
|
+
if (!isPlainObject(topology) || topology.status !== "confirmed") return 0;
|
|
81
|
+
let unscored = 0;
|
|
82
|
+
for (const component of asArray(topology.components)) {
|
|
83
|
+
if (!isPlainObject(component) || component.status === "deferred") continue;
|
|
84
|
+
const clarity = isPlainObject(component.clarity_scores) ? component.clarity_scores : {};
|
|
85
|
+
const incomplete = CORE_CLARITY_DIMENSIONS.some(dimension => {
|
|
86
|
+
const score = clarity[dimension];
|
|
87
|
+
return typeof score !== "number" || !Number.isFinite(score);
|
|
88
|
+
});
|
|
89
|
+
if (incomplete) unscored += 1;
|
|
90
|
+
}
|
|
91
|
+
return unscored;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function autoAnswerRatio(inner: Record<string, unknown>): number {
|
|
95
|
+
const autoAnswered = asArray(inner.auto_answered_rounds).length;
|
|
96
|
+
if (autoAnswered === 0) return 0;
|
|
97
|
+
const scored = asArray(inner.rounds).filter(round => isPlainObject(round) && round.lifecycle === "scored").length;
|
|
98
|
+
return Math.min(1, autoAnswered / Math.max(scored, 1));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Floor computation + clamping
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
/** Compute the deterministic floor from the inner deep-interview `state` object. */
|
|
106
|
+
export function computeAmbiguityFloor(inner: unknown): AmbiguityFloorBreakdown {
|
|
107
|
+
const state = isPlainObject(inner) ? inner : {};
|
|
108
|
+
const disputedFactCount = asArray(state.established_facts).filter(isUnresolvedDisputedFact).length;
|
|
109
|
+
const unscoredActiveComponentCount = countUnscoredActiveComponents(state.topology);
|
|
110
|
+
const ratio = autoAnswerRatio(state);
|
|
111
|
+
const floor =
|
|
112
|
+
DISPUTED_FACT_WEIGHT * disputedFactCount +
|
|
113
|
+
UNSCORED_COMPONENT_WEIGHT * unscoredActiveComponentCount +
|
|
114
|
+
AUTO_ANSWER_DILUTION_WEIGHT * ratio;
|
|
115
|
+
return {
|
|
116
|
+
floor: round2(Math.min(1, Math.max(0, floor))),
|
|
117
|
+
disputed_fact_count: disputedFactCount,
|
|
118
|
+
unscored_active_component_count: unscoredActiveComponentCount,
|
|
119
|
+
auto_answer_ratio: round2(ratio),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Clamp an LLM-reported ambiguity to the deterministic floor: `max(reported, floor)`. */
|
|
124
|
+
export function clampReportedAmbiguity(reported: number, floor: number): AmbiguityClampResult {
|
|
125
|
+
const bounded = Math.min(1, Math.max(0, reported));
|
|
126
|
+
if (floor > bounded) return { effective: Math.min(1, floor), clamped: true };
|
|
127
|
+
return { effective: bounded, clamped: false };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Mark every established fact recorded in `retractedRound` as disputed. This is the
|
|
132
|
+
* mechanical contradiction signal for answer retraction: when the user replaces an
|
|
133
|
+
* already-scored answer (A -> B pivot), the facts that answer established can no
|
|
134
|
+
* longer be trusted, so they raise the floor until re-confirmed or superseded.
|
|
135
|
+
* Returns new arrays/objects; never mutates the input.
|
|
136
|
+
*/
|
|
137
|
+
export function disputeFactsFromRetractedRound(
|
|
138
|
+
facts: readonly unknown[],
|
|
139
|
+
retractedRound: number,
|
|
140
|
+
): { facts: Record<string, unknown>[]; disputedIds: string[] } {
|
|
141
|
+
const disputedIds: string[] = [];
|
|
142
|
+
const next = facts.filter(isPlainObject).map(fact => {
|
|
143
|
+
const record = fact as Record<string, unknown>;
|
|
144
|
+
if (record.round !== retractedRound || record.disputed === true) return { ...record };
|
|
145
|
+
if (typeof record.superseded_by === "string" && record.superseded_by.trim() !== "") return { ...record };
|
|
146
|
+
if (typeof record.id === "string") disputedIds.push(record.id);
|
|
147
|
+
return { ...record, disputed: true };
|
|
148
|
+
});
|
|
149
|
+
return { facts: next, disputedIds };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface AppliedAmbiguityFloor {
|
|
153
|
+
envelope: DeepInterviewStateEnvelope;
|
|
154
|
+
breakdown: AmbiguityFloorBreakdown;
|
|
155
|
+
clamped: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Enforce the floor invariant on a full deep-interview envelope: recompute the floor
|
|
160
|
+
* from persisted evidence, clamp `state.current_ambiguity`, and clamp the latest
|
|
161
|
+
* scored round (preserving the original value as `reported_ambiguity` for audit).
|
|
162
|
+
* Historical rounds are never rewritten. Idempotent and non-mutating; every writer
|
|
163
|
+
* (state CLI write/reconcile, recorder) applies this immediately before persisting.
|
|
164
|
+
*/
|
|
165
|
+
export function applyAmbiguityFloorToEnvelope(value: unknown): AppliedAmbiguityFloor {
|
|
166
|
+
const envelope = normalizeDeepInterviewEnvelope(value);
|
|
167
|
+
const inner = { ...(envelope.state as Record<string, unknown>) };
|
|
168
|
+
const breakdown = computeAmbiguityFloor(inner);
|
|
169
|
+
let clamped = false;
|
|
170
|
+
|
|
171
|
+
const rounds = asArray(inner.rounds).filter(isPlainObject) as unknown as DeepInterviewRoundRecord[];
|
|
172
|
+
let latestScoredIndex = -1;
|
|
173
|
+
for (let index = 0; index < rounds.length; index += 1) {
|
|
174
|
+
const candidate = rounds[index];
|
|
175
|
+
if (candidate.lifecycle !== "scored" || !Number.isFinite(candidate.round)) continue;
|
|
176
|
+
if (latestScoredIndex < 0 || candidate.round >= rounds[latestScoredIndex].round) latestScoredIndex = index;
|
|
177
|
+
}
|
|
178
|
+
if (latestScoredIndex >= 0) {
|
|
179
|
+
const latest = rounds[latestScoredIndex];
|
|
180
|
+
if (typeof latest.ambiguity === "number") {
|
|
181
|
+
const clampedRound = clampReportedAmbiguity(latest.ambiguity, breakdown.floor);
|
|
182
|
+
if (clampedRound.clamped) {
|
|
183
|
+
const nextRounds = [...rounds];
|
|
184
|
+
nextRounds[latestScoredIndex] = {
|
|
185
|
+
...latest,
|
|
186
|
+
reported_ambiguity: latest.reported_ambiguity ?? latest.ambiguity,
|
|
187
|
+
ambiguity: clampedRound.effective,
|
|
188
|
+
ambiguity_floor: breakdown.floor,
|
|
189
|
+
};
|
|
190
|
+
inner.rounds = nextRounds;
|
|
191
|
+
clamped = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof inner.current_ambiguity === "number") {
|
|
197
|
+
const clampedCurrent = clampReportedAmbiguity(inner.current_ambiguity, breakdown.floor);
|
|
198
|
+
if (clampedCurrent.clamped) {
|
|
199
|
+
inner.current_ambiguity = clampedCurrent.effective;
|
|
200
|
+
clamped = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
inner.ambiguity_floor = breakdown;
|
|
205
|
+
return { envelope: { ...envelope, state: inner }, breakdown, clamped };
|
|
206
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
2
2
|
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
3
3
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
4
|
+
import {
|
|
5
|
+
clampReportedAmbiguity,
|
|
6
|
+
computeAmbiguityFloor,
|
|
7
|
+
disputeFactsFromRetractedRound,
|
|
8
|
+
} from "./deep-interview-ambiguity";
|
|
4
9
|
import {
|
|
5
10
|
answerHash,
|
|
6
11
|
type DeepInterviewEstablishedFact,
|
|
@@ -14,6 +19,7 @@ import {
|
|
|
14
19
|
import { writeSessionActivityMarker } from "./session-resolution";
|
|
15
20
|
import { readExistingStateForMutation, writeGuardedWorkflowEnvelopeAtomic } from "./state-writer";
|
|
16
21
|
|
|
22
|
+
export * from "./deep-interview-ambiguity";
|
|
17
23
|
export * from "./deep-interview-state";
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -395,30 +401,55 @@ export async function syncDeepInterviewRecorderHud(
|
|
|
395
401
|
await syncRecorderHud(cwd, normalizeDeepInterviewEnvelope(read.value), sessionId);
|
|
396
402
|
}
|
|
397
403
|
|
|
398
|
-
/**
|
|
404
|
+
/**
|
|
405
|
+
* Record an `answered` shell for one round (append-or-merge by durable key).
|
|
406
|
+
*
|
|
407
|
+
* Retraction dynamics: replacing an already-`scored` answer is a mechanical
|
|
408
|
+
* contradiction signal (the user pivoted). The facts that round established are
|
|
409
|
+
* marked disputed, which raises the deterministic ambiguity floor immediately —
|
|
410
|
+
* without waiting for the LLM scorer to self-report a trigger.
|
|
411
|
+
*/
|
|
399
412
|
export async function appendOrMergeDeepInterviewRound(
|
|
400
413
|
cwd: string,
|
|
401
414
|
statePath: string,
|
|
402
415
|
input: DeepInterviewAnswerInput,
|
|
403
416
|
options: { sessionId?: string } = {},
|
|
404
|
-
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord }> {
|
|
417
|
+
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord; disputedFactIds?: string[] }> {
|
|
405
418
|
const envelope = await readEnvelope(statePath);
|
|
406
419
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
407
420
|
const shell = buildAnswerShell({ ...input, interviewId });
|
|
408
421
|
const rounds = readRounds(envelope);
|
|
422
|
+
const priorRecord = rounds.find(r => r.round_key === shell.round_key);
|
|
409
423
|
const result = appendOrMergeRound(rounds, shell);
|
|
410
424
|
if (result.action === "noop") {
|
|
411
425
|
await repairRecorderHudFromPersisted(cwd, statePath, options.sessionId);
|
|
412
426
|
return { action: result.action, record: result.record };
|
|
413
427
|
}
|
|
414
|
-
|
|
428
|
+
const inner = envelope.state as Record<string, unknown>;
|
|
429
|
+
inner.rounds = result.rounds;
|
|
430
|
+
let disputedFactIds: string[] | undefined;
|
|
431
|
+
if (result.action === "replaced" && priorRecord?.lifecycle === "scored") {
|
|
432
|
+
const facts = Array.isArray(inner.established_facts) ? inner.established_facts : [];
|
|
433
|
+
const disputed = disputeFactsFromRetractedRound(facts, shell.round);
|
|
434
|
+
if (disputed.disputedIds.length > 0) {
|
|
435
|
+
inner.established_facts = disputed.facts;
|
|
436
|
+
disputedFactIds = disputed.disputedIds;
|
|
437
|
+
// Surface the rise right away: the retraction changed the evidence, so the
|
|
438
|
+
// gating score must reflect the new floor before the next scoring pass.
|
|
439
|
+
const breakdown = computeAmbiguityFloor(inner);
|
|
440
|
+
inner.ambiguity_floor = breakdown;
|
|
441
|
+
if (typeof inner.current_ambiguity === "number") {
|
|
442
|
+
inner.current_ambiguity = clampReportedAmbiguity(inner.current_ambiguity, breakdown.floor).effective;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
415
446
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview record-answer");
|
|
416
447
|
try {
|
|
417
448
|
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
418
449
|
} catch {
|
|
419
450
|
// HUD sync is best-effort cache maintenance and must not change record semantics.
|
|
420
451
|
}
|
|
421
|
-
return { action: result.action, record: result.record };
|
|
452
|
+
return { action: result.action, record: result.record, disputedFactIds };
|
|
422
453
|
}
|
|
423
454
|
|
|
424
455
|
/**
|
|
@@ -459,12 +490,34 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
459
490
|
const envelope = await readEnvelope(statePath);
|
|
460
491
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
461
492
|
const rounds = readRounds(envelope);
|
|
462
|
-
const { rounds:
|
|
493
|
+
const { rounds: enrichedRounds, record: reportedRecord } = enrichRoundWithScoring(rounds, {
|
|
494
|
+
...input,
|
|
495
|
+
interviewId,
|
|
496
|
+
});
|
|
497
|
+
// Deterministic floor (Ouroboros principle): the LLM-reported ambiguity is only a
|
|
498
|
+
// lower-bound input. Evidence persisted in state — unresolved disputed facts,
|
|
499
|
+
// unscored active components, auto-answer dilution — sets a code-computed floor
|
|
500
|
+
// the reported score can never fall under, so a pivot mechanically raises the
|
|
501
|
+
// gating score even when the scorer omits triggers.
|
|
502
|
+
const inner = envelope.state as Record<string, unknown>;
|
|
503
|
+
const breakdown = computeAmbiguityFloor({ ...inner, rounds: enrichedRounds });
|
|
504
|
+
const clampResult = clampReportedAmbiguity(input.ambiguity, breakdown.floor);
|
|
505
|
+
let record = reportedRecord;
|
|
506
|
+
let nextRounds = enrichedRounds;
|
|
507
|
+
if (clampResult.clamped) {
|
|
508
|
+
record = {
|
|
509
|
+
...reportedRecord,
|
|
510
|
+
ambiguity: clampResult.effective,
|
|
511
|
+
reported_ambiguity: input.ambiguity,
|
|
512
|
+
ambiguity_floor: breakdown.floor,
|
|
513
|
+
};
|
|
514
|
+
nextRounds = enrichedRounds.map(item => (item.round_key === record.round_key ? record : item));
|
|
515
|
+
}
|
|
463
516
|
// Fail closed: a scored transition that violates the bidirectional invariant
|
|
464
517
|
// (an active trigger that improves the affected dimension or fails to raise
|
|
465
518
|
// overall ambiguity, or a disputed/unresolved trigger lacking a rationale) must
|
|
466
519
|
// never be persisted — storing it lets the interview falsely converge. Validate
|
|
467
|
-
// against the most recent prior scored round
|
|
520
|
+
// the effective (floor-clamped) record against the most recent prior scored round.
|
|
468
521
|
const prior = latestPriorScoredRound(rounds, record.round_key, record.round);
|
|
469
522
|
const validation = validateDeepInterviewScoredTransition(prior, record);
|
|
470
523
|
if (!validation.ok) {
|
|
@@ -472,8 +525,9 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
472
525
|
`deep-interview scored transition for round ${record.round} is invalid and was refused: ${validation.violations.join("; ")}`,
|
|
473
526
|
);
|
|
474
527
|
}
|
|
475
|
-
|
|
476
|
-
|
|
528
|
+
inner.rounds = nextRounds;
|
|
529
|
+
inner.current_ambiguity = clampResult.effective;
|
|
530
|
+
inner.ambiguity_floor = breakdown;
|
|
477
531
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview score-round");
|
|
478
532
|
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
479
533
|
return { record };
|