@gajae-code/coding-agent 0.11.8 → 0.11.10
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 +35 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/state-writer.d.ts +45 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +8 -1
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +74 -5
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +13 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +13 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +3 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +1 -0
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +32 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/model-profiles.ts +9 -10
- package/src/config/model-registry.ts +4 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +843 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +310 -227
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/ralplan-runtime.ts +3 -1
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +29 -9
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +60 -5
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +685 -120
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +12 -0
- package/src/session/agent-session.ts +140 -10
- package/src/session/internal/managed-session-scope.ts +339 -228
- package/src/session/internal/managed-session-storage.ts +17 -4
- package/src/session/session-manager.ts +96 -33
- package/src/session/session-storage.ts +23 -0
- package/src/skill-state/active-state.ts +10 -0
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +154 -44
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +3 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
2
2
|
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
3
|
-
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
4
|
-
import {
|
|
5
|
-
clampReportedAmbiguity,
|
|
6
|
-
computeAmbiguityFloor,
|
|
7
|
-
disputeFactsFromRetractedRound,
|
|
8
|
-
} from "./deep-interview-ambiguity";
|
|
9
3
|
import {
|
|
10
4
|
answerHash,
|
|
5
|
+
applyDeepInterviewRoundResultV1,
|
|
11
6
|
assertDeepInterviewInputWithinLimit,
|
|
12
7
|
assertDeepInterviewIntentManifest,
|
|
13
8
|
assertDeepInterviewStructuredResponseWithinLimit,
|
|
@@ -16,8 +11,10 @@ import {
|
|
|
16
11
|
type DeepInterviewIntentItem,
|
|
17
12
|
type DeepInterviewIntentSubstitution,
|
|
18
13
|
type DeepInterviewRoundRecord,
|
|
14
|
+
type DeepInterviewRoundResultV1,
|
|
19
15
|
type DeepInterviewStateEnvelope,
|
|
20
16
|
type DeepInterviewTriggerMetadata,
|
|
17
|
+
deepInterviewAnswerIdentityEqual,
|
|
21
18
|
deriveRoundKey,
|
|
22
19
|
MAX_USER_RESPONSE_LENGTH,
|
|
23
20
|
normalizeDeepInterviewEnvelope,
|
|
@@ -25,7 +22,7 @@ import {
|
|
|
25
22
|
reviewDeepInterviewIntent,
|
|
26
23
|
} from "./deep-interview-state";
|
|
27
24
|
import { writeSessionActivityMarker } from "./session-resolution";
|
|
28
|
-
import { readExistingStateForMutation,
|
|
25
|
+
import { readExistingStateForMutation, transformGuardedWorkflowEnvelopeAtomic } from "./state-writer";
|
|
29
26
|
|
|
30
27
|
export * from "./deep-interview-ambiguity";
|
|
31
28
|
export * from "./deep-interview-state";
|
|
@@ -72,6 +69,8 @@ export interface DeepInterviewScoringInput {
|
|
|
72
69
|
scores: Record<string, number>;
|
|
73
70
|
ambiguity: number;
|
|
74
71
|
triggers?: DeepInterviewTriggerMetadata[];
|
|
72
|
+
/** Complete native round result for typed callers. */
|
|
73
|
+
roundResult?: DeepInterviewRoundResultV1;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
export type AppendOrMergeAction = "created" | "noop" | "replaced";
|
|
@@ -102,6 +101,21 @@ export interface TransitionValidationResult {
|
|
|
102
101
|
// Pure helpers: records
|
|
103
102
|
// =============================================================================
|
|
104
103
|
|
|
104
|
+
/**
|
|
105
|
+
* `deepInterview.dimension` is agent-supplied free text on post-topology asks, but the persisted
|
|
106
|
+
* envelope only accepts canonical dimension ids. Fold obvious case/label variants down, and drop
|
|
107
|
+
* anything unrecognized rather than persisting a value the validator rejects.
|
|
108
|
+
*/
|
|
109
|
+
function canonicalDimension(value: string | undefined): string | undefined {
|
|
110
|
+
if (value === undefined) return undefined;
|
|
111
|
+
const normalized = value.trim().toLowerCase();
|
|
112
|
+
if (normalized === "topology") return "topology";
|
|
113
|
+
for (const dimension of ["goal", "constraints", "criteria", "context"]) {
|
|
114
|
+
if (normalized === dimension || normalized.startsWith(`${dimension} `)) return dimension;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
105
119
|
export function buildAnswerShell(
|
|
106
120
|
input: DeepInterviewAnswerInput,
|
|
107
121
|
now: string = new Date().toISOString(),
|
|
@@ -117,7 +131,7 @@ export function buildAnswerShell(
|
|
|
117
131
|
selected_options: input.selectedOptions,
|
|
118
132
|
custom_input: input.customInput,
|
|
119
133
|
component: input.component,
|
|
120
|
-
dimension: input.dimension,
|
|
134
|
+
dimension: canonicalDimension(input.dimension),
|
|
121
135
|
ambiguity_at_ask: input.ambiguity,
|
|
122
136
|
lifecycle: "answered",
|
|
123
137
|
answered_at: now,
|
|
@@ -125,11 +139,9 @@ export function buildAnswerShell(
|
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
/**
|
|
128
|
-
* Append-or-merge by `round_key`.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* - same key, different hashes -> deterministic replacement of the prior shell
|
|
132
|
-
* (`replaced`); the prior answer for that key is superseded and lifecycle resets.
|
|
142
|
+
* Append-or-merge by `round_key`. An answer is a no-op only when its complete
|
|
143
|
+
* canonical shell identity matches; a scored record is immutable evidence and
|
|
144
|
+
* cannot be replaced by a late answer.
|
|
133
145
|
*/
|
|
134
146
|
export function appendOrMergeRound(
|
|
135
147
|
rounds: readonly DeepInterviewRoundRecord[],
|
|
@@ -142,9 +154,10 @@ export function appendOrMergeRound(
|
|
|
142
154
|
return { rounds: next, action: "created", record: shell };
|
|
143
155
|
}
|
|
144
156
|
const existing = next[index];
|
|
145
|
-
if (existing
|
|
157
|
+
if (deepInterviewAnswerIdentityEqual(existing, shell) && existing.answer_hash === shell.answer_hash) {
|
|
146
158
|
return { rounds: next, action: "noop", record: existing };
|
|
147
159
|
}
|
|
160
|
+
if (existing.lifecycle === "scored") throw new Error("DI_ANSWER_LIFECYCLE_CONFLICT");
|
|
148
161
|
next[index] = shell;
|
|
149
162
|
return { rounds: next, action: "replaced", record: shell };
|
|
150
163
|
}
|
|
@@ -224,10 +237,10 @@ export function validateDeepInterviewScoredTransition(
|
|
|
224
237
|
`active trigger ${trigger.kind} did not raise ambiguity (${prior.ambiguity} -> ${next.ambiguity})`,
|
|
225
238
|
);
|
|
226
239
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
const priorDim = prior.scores?.[trigger.dimension]
|
|
230
|
-
const nextDim = next.scores?.[trigger.dimension]
|
|
240
|
+
// The runtime derives both dimension values from scored records; callers cannot
|
|
241
|
+
// provide parallel trigger metrics.
|
|
242
|
+
const priorDim = prior.scores?.[trigger.dimension];
|
|
243
|
+
const nextDim = next.scores?.[trigger.dimension];
|
|
231
244
|
if (typeof priorDim !== "number" || typeof nextDim !== "number") {
|
|
232
245
|
violations.push(
|
|
233
246
|
`active trigger ${trigger.kind} is missing dimension "${trigger.dimension}" scores to prove non-improvement`,
|
|
@@ -304,19 +317,16 @@ export function projectCompactState(value: unknown, options: { lastN?: number }
|
|
|
304
317
|
// Persistence wrappers (state-writer backed; runtime-owned)
|
|
305
318
|
// =============================================================================
|
|
306
319
|
|
|
320
|
+
interface RecorderMutationOptions {
|
|
321
|
+
sessionId?: string;
|
|
322
|
+
expectedRevision?: number;
|
|
323
|
+
}
|
|
324
|
+
|
|
307
325
|
async function readEnvelope(statePath: string): Promise<DeepInterviewStateEnvelope> {
|
|
308
326
|
const read = await readExistingStateForMutation(statePath);
|
|
309
327
|
if (read.kind === "valid") return ensureDeepInterviewStateShape(read.value);
|
|
310
|
-
if (read.kind === "
|
|
311
|
-
|
|
312
|
-
// (e.g. the ask tool) catch this and warn without mutating, preserving the file
|
|
313
|
-
// for recovery. Only a genuinely absent file is defaulted below.
|
|
314
|
-
throw new Error(
|
|
315
|
-
`deep-interview state at ${statePath} is corrupt or tampered (${read.error}); refusing to overwrite`,
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
// Absent: start from a defaulted shape.
|
|
319
|
-
return ensureDeepInterviewStateShape(undefined);
|
|
328
|
+
if (read.kind === "absent") throw new Error("DI_STATE_ABSENT");
|
|
329
|
+
throw new Error(`deep-interview state at ${statePath} is corrupt or tampered (${read.error})`);
|
|
320
330
|
}
|
|
321
331
|
|
|
322
332
|
function existingStateRevision(value: unknown): number | undefined {
|
|
@@ -329,49 +339,16 @@ function interviewIdOf(envelope: DeepInterviewStateEnvelope): string | undefined
|
|
|
329
339
|
const inner = (envelope.state ?? {}) as Record<string, unknown>;
|
|
330
340
|
return typeof inner.interview_id === "string" ? inner.interview_id : undefined;
|
|
331
341
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
statePath: string,
|
|
336
|
-
envelope: DeepInterviewStateEnvelope,
|
|
337
|
-
sessionId: string | undefined,
|
|
338
|
-
command: string,
|
|
339
|
-
): Promise<void> {
|
|
340
|
-
if (!sessionId) throw new Error("deep-interview recorder requires a session id");
|
|
341
|
-
const now = new Date().toISOString();
|
|
342
|
-
const payload: Record<string, unknown> = { ...normalizeDeepInterviewEnvelope(envelope), updated_at: now };
|
|
343
|
-
// Guarantee RequiredOnWriteEnvelopeSchema fields for the fresh/absent fallback;
|
|
344
|
-
// existing real state already carries these and is preserved by the spread above.
|
|
345
|
-
payload.skill ??= "deep-interview";
|
|
346
|
-
payload.version ??= WORKFLOW_STATE_VERSION;
|
|
347
|
-
payload.active ??= true;
|
|
348
|
-
payload.current_phase ??= "interviewing";
|
|
349
|
-
const expectedRevision = existingStateRevision(envelope);
|
|
350
|
-
const writeResult = await writeGuardedWorkflowEnvelopeAtomic(statePath, payload, {
|
|
351
|
-
cwd,
|
|
352
|
-
policy: "source",
|
|
353
|
-
expectedRevision,
|
|
354
|
-
receipt: { cwd, skill: "deep-interview", owner: "gjc-runtime", command, sessionId, nowIso: now },
|
|
355
|
-
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview", sessionId },
|
|
356
|
-
});
|
|
357
|
-
// Reflect the freshly written revision back onto the in-memory envelope so a
|
|
358
|
-
// follow-up HUD sync derives its `sourceRevision` from the persisted revision
|
|
359
|
-
// (not the stale pre-write value), otherwise the active-state writer treats the
|
|
360
|
-
// newer HUD as stale and skips it (e.g. dropping the ambiguity chip after scoring).
|
|
361
|
-
if (writeResult.written && typeof expectedRevision === "number") {
|
|
362
|
-
(envelope as Record<string, unknown>).state_revision = expectedRevision + 1;
|
|
363
|
-
}
|
|
364
|
-
await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-recorder", path: statePath });
|
|
342
|
+
interface RecorderHudSyncSlot {
|
|
343
|
+
latestCommittedRevision: number;
|
|
344
|
+
tail: Promise<void>;
|
|
365
345
|
}
|
|
366
346
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
* from the complete normalized mode-state envelope. HUD is a cache; a failure here
|
|
370
|
-
* must never change durable record semantics.
|
|
371
|
-
*/
|
|
347
|
+
const recorderHudSyncSlots = new Map<string, RecorderHudSyncSlot>();
|
|
348
|
+
|
|
372
349
|
async function syncRecorderHud(
|
|
373
350
|
cwd: string,
|
|
374
|
-
envelope:
|
|
351
|
+
envelope: Record<string, unknown>,
|
|
375
352
|
sessionId: string | undefined,
|
|
376
353
|
): Promise<void> {
|
|
377
354
|
const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
|
|
@@ -382,26 +359,65 @@ async function syncRecorderHud(
|
|
|
382
359
|
phase,
|
|
383
360
|
sessionId,
|
|
384
361
|
source: "gjc-runtime-deep-interview-recorder",
|
|
385
|
-
hud: deriveDeepInterviewHud(
|
|
386
|
-
|
|
362
|
+
hud: deriveDeepInterviewHud(envelope, { phase }),
|
|
363
|
+
...(typeof envelope.state_revision === "number" ? { committedModeRevision: envelope.state_revision } : {}),
|
|
387
364
|
});
|
|
388
365
|
}
|
|
389
366
|
|
|
390
|
-
|
|
391
|
-
* Repair the cached HUD after a no-op append. A no-op writes no mode-state, so the
|
|
392
|
-
* HUD is derived from a fresh read of the current persisted state (never from the
|
|
393
|
-
* pre-noop in-memory envelope) to avoid overwriting newer active-state with stale values.
|
|
394
|
-
*/
|
|
395
|
-
async function repairRecorderHudFromPersisted(
|
|
367
|
+
async function syncRecorderHudAtCommittedRevision(
|
|
396
368
|
cwd: string,
|
|
397
|
-
|
|
398
|
-
sessionId: string
|
|
369
|
+
envelope: Record<string, unknown>,
|
|
370
|
+
sessionId: string,
|
|
371
|
+
revision: number,
|
|
399
372
|
): Promise<void> {
|
|
373
|
+
const key = `${cwd}\0${sessionId}`;
|
|
374
|
+
const slot = recorderHudSyncSlots.get(key) ?? { latestCommittedRevision: -1, tail: Promise.resolve() };
|
|
375
|
+
recorderHudSyncSlots.set(key, slot);
|
|
376
|
+
const sync = slot.tail
|
|
377
|
+
.catch(() => undefined)
|
|
378
|
+
.then(async () => {
|
|
379
|
+
if (revision <= slot.latestCommittedRevision) return;
|
|
380
|
+
await syncRecorderHud(cwd, { ...envelope, state_revision: revision }, sessionId);
|
|
381
|
+
slot.latestCommittedRevision = revision;
|
|
382
|
+
});
|
|
383
|
+
slot.tail = sync;
|
|
384
|
+
await sync;
|
|
385
|
+
}
|
|
386
|
+
export interface DeepInterviewPostCommitWarning {
|
|
387
|
+
code: "DI_POST_COMMIT_AUDIT_FAILED" | "DI_POST_COMMIT_ACTIVITY_FAILED" | "DI_POST_COMMIT_HUD_FAILED";
|
|
388
|
+
message: string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function runDeepInterviewPostCommitEffects(options: {
|
|
392
|
+
cwd: string;
|
|
393
|
+
statePath: string;
|
|
394
|
+
sessionId: string;
|
|
395
|
+
envelope: Record<string, unknown>;
|
|
396
|
+
revision: number;
|
|
397
|
+
writer: string;
|
|
398
|
+
auditWarnings?: readonly { code: "DI_POST_COMMIT_AUDIT_FAILED"; message: string }[];
|
|
399
|
+
}): Promise<DeepInterviewPostCommitWarning[]> {
|
|
400
|
+
const warnings: DeepInterviewPostCommitWarning[] = [...(options.auditWarnings ?? [])];
|
|
401
|
+
try {
|
|
402
|
+
await writeSessionActivityMarker(options.cwd, options.sessionId, {
|
|
403
|
+
writer: options.writer,
|
|
404
|
+
path: options.statePath,
|
|
405
|
+
});
|
|
406
|
+
} catch (error) {
|
|
407
|
+
warnings.push({
|
|
408
|
+
code: "DI_POST_COMMIT_ACTIVITY_FAILED",
|
|
409
|
+
message: error instanceof Error ? error.message : String(error),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
400
412
|
try {
|
|
401
|
-
await
|
|
402
|
-
} catch {
|
|
403
|
-
|
|
413
|
+
await syncRecorderHudAtCommittedRevision(options.cwd, options.envelope, options.sessionId, options.revision);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
warnings.push({
|
|
416
|
+
code: "DI_POST_COMMIT_HUD_FAILED",
|
|
417
|
+
message: error instanceof Error ? error.message : String(error),
|
|
418
|
+
});
|
|
404
419
|
}
|
|
420
|
+
return warnings;
|
|
405
421
|
}
|
|
406
422
|
|
|
407
423
|
/** Refresh the best-effort HUD cache from persisted deep-interview state. */
|
|
@@ -412,110 +428,129 @@ export async function syncDeepInterviewRecorderHud(
|
|
|
412
428
|
): Promise<void> {
|
|
413
429
|
const read = await readExistingStateForMutation(statePath);
|
|
414
430
|
if (read.kind !== "valid") return;
|
|
415
|
-
|
|
431
|
+
const envelope = normalizeDeepInterviewEnvelope(read.value);
|
|
432
|
+
const revision = existingStateRevision(read.value);
|
|
433
|
+
if (revision === undefined || !sessionId) return;
|
|
434
|
+
await syncRecorderHudAtCommittedRevision(cwd, envelope, sessionId, revision);
|
|
416
435
|
}
|
|
417
436
|
|
|
418
437
|
/**
|
|
419
438
|
* Record an `answered` shell for one round (append-or-merge by durable key).
|
|
420
439
|
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* marked disputed, which raises the deterministic ambiguity floor immediately —
|
|
424
|
-
* without waiting for the LLM scorer to self-report a trigger.
|
|
440
|
+
* Replacing an already-scored answer is rejected with
|
|
441
|
+
* `DI_ANSWER_LIFECYCLE_CONFLICT`; scored evidence is immutable.
|
|
425
442
|
*/
|
|
426
443
|
export async function appendOrMergeDeepInterviewRound(
|
|
427
444
|
cwd: string,
|
|
428
445
|
statePath: string,
|
|
429
446
|
input: DeepInterviewAnswerInput,
|
|
430
|
-
options:
|
|
431
|
-
): Promise<{
|
|
447
|
+
options: RecorderMutationOptions = {},
|
|
448
|
+
): Promise<{
|
|
449
|
+
action: AppendOrMergeAction;
|
|
450
|
+
record: DeepInterviewRoundRecord;
|
|
451
|
+
warnings: DeepInterviewPostCommitWarning[];
|
|
452
|
+
}> {
|
|
432
453
|
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
433
454
|
if (input.customInput !== undefined)
|
|
434
455
|
assertDeepInterviewInputWithinLimit(input.customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
status: approved ? "approved" : "pending",
|
|
480
|
-
supporting_substitutions: input.intent_review.supporting_substitutions,
|
|
481
|
-
...(approved
|
|
482
|
-
? {
|
|
483
|
-
approval_round: input.round,
|
|
456
|
+
if (!options.sessionId) throw new Error("deep-interview recorder requires a session id");
|
|
457
|
+
const initial = await readEnvelope(statePath);
|
|
458
|
+
let result: AppendOrMergeResult | undefined;
|
|
459
|
+
const now = new Date().toISOString();
|
|
460
|
+
const committed = await transformGuardedWorkflowEnvelopeAtomic(statePath, {
|
|
461
|
+
cwd,
|
|
462
|
+
expectedRevision: options.expectedRevision ?? existingStateRevision(initial) ?? 0,
|
|
463
|
+
receipt: {
|
|
464
|
+
cwd,
|
|
465
|
+
skill: "deep-interview",
|
|
466
|
+
owner: "gjc-runtime",
|
|
467
|
+
command: "gjc deep-interview record-answer",
|
|
468
|
+
sessionId: options.sessionId,
|
|
469
|
+
nowIso: now,
|
|
470
|
+
},
|
|
471
|
+
audit: {
|
|
472
|
+
category: "state",
|
|
473
|
+
verb: "write",
|
|
474
|
+
owner: "gjc-runtime",
|
|
475
|
+
skill: "deep-interview",
|
|
476
|
+
sessionId: options.sessionId,
|
|
477
|
+
},
|
|
478
|
+
transform: current => {
|
|
479
|
+
const envelope = ensureDeepInterviewStateShape(current);
|
|
480
|
+
const shell = buildAnswerShell(
|
|
481
|
+
{
|
|
482
|
+
...input,
|
|
483
|
+
interviewId: input.interviewId ?? interviewIdOf(envelope),
|
|
484
|
+
customInput: input.intent_contract || input.intent_review ? undefined : input.customInput,
|
|
485
|
+
},
|
|
486
|
+
now,
|
|
487
|
+
);
|
|
488
|
+
result = appendOrMergeRound(readRounds(envelope), shell);
|
|
489
|
+
const state = envelope.state as Record<string, unknown>;
|
|
490
|
+
let intentStateChanged = false;
|
|
491
|
+
if (input.intent_contract) {
|
|
492
|
+
if (input.round !== 0 || input.component !== "review-topology" || input.dimension !== "topology")
|
|
493
|
+
throw new Error("intent contract requires Round 0 topology metadata");
|
|
494
|
+
const confirmed =
|
|
495
|
+
input.selectedOptions?.length === 1 &&
|
|
496
|
+
input.intent_contract.confirmation_options.includes(input.selectedOptions[0]);
|
|
497
|
+
if (confirmed) {
|
|
498
|
+
const contract = createDeepInterviewIntentManifest(input.intent_contract.items, {
|
|
499
|
+
round: 0,
|
|
484
500
|
answer_hash: shell.answer_hash,
|
|
485
|
-
|
|
501
|
+
});
|
|
502
|
+
const existingContract = state.intent_contract;
|
|
503
|
+
if (existingContract !== undefined) {
|
|
504
|
+
assertDeepInterviewIntentManifest(existingContract);
|
|
505
|
+
if (
|
|
506
|
+
existingContract.digest !== contract.digest ||
|
|
507
|
+
existingContract.confirmation_answer_hash !== contract.confirmation_answer_hash
|
|
508
|
+
)
|
|
509
|
+
throw new Error("locked intent contract cannot be replaced");
|
|
510
|
+
} else {
|
|
511
|
+
state.intent_contract = contract;
|
|
512
|
+
intentStateChanged = true;
|
|
486
513
|
}
|
|
487
|
-
|
|
488
|
-
});
|
|
489
|
-
intentStateChanged = true;
|
|
490
|
-
}
|
|
491
|
-
if (result.action === "noop" && !intentStateChanged) {
|
|
492
|
-
await repairRecorderHudFromPersisted(cwd, statePath, options.sessionId);
|
|
493
|
-
return { action: result.action, record: result.record };
|
|
494
|
-
}
|
|
495
|
-
inner.rounds = result.rounds;
|
|
496
|
-
let disputedFactIds: string[] | undefined;
|
|
497
|
-
if (result.action === "replaced" && priorRecord?.lifecycle === "scored") {
|
|
498
|
-
const facts = Array.isArray(inner.established_facts) ? inner.established_facts : [];
|
|
499
|
-
const disputed = disputeFactsFromRetractedRound(facts, shell.round);
|
|
500
|
-
if (disputed.disputedIds.length > 0) {
|
|
501
|
-
inner.established_facts = disputed.facts;
|
|
502
|
-
disputedFactIds = disputed.disputedIds;
|
|
503
|
-
// Surface the rise right away: the retraction changed the evidence, so the
|
|
504
|
-
// gating score must reflect the new floor before the next scoring pass.
|
|
505
|
-
const breakdown = computeAmbiguityFloor(inner);
|
|
506
|
-
inner.ambiguity_floor = breakdown;
|
|
507
|
-
if (typeof inner.current_ambiguity === "number") {
|
|
508
|
-
inner.current_ambiguity = clampReportedAmbiguity(inner.current_ambiguity, breakdown.floor).effective;
|
|
514
|
+
}
|
|
509
515
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
516
|
+
if (input.intent_review) {
|
|
517
|
+
if (input.round <= 0) throw new Error("intent review requires a post-Round-0 answer");
|
|
518
|
+
const locked = state.intent_contract;
|
|
519
|
+
assertDeepInterviewIntentManifest(locked);
|
|
520
|
+
const approved =
|
|
521
|
+
input.selectedOptions?.length === 1 &&
|
|
522
|
+
input.intent_review.approval_options.includes(input.selectedOptions[0]);
|
|
523
|
+
state.intent_review = reviewDeepInterviewIntent(locked, input.intent_review.observed_items, {
|
|
524
|
+
status: approved ? "approved" : "pending",
|
|
525
|
+
supporting_substitutions: input.intent_review.supporting_substitutions,
|
|
526
|
+
...(approved
|
|
527
|
+
? {
|
|
528
|
+
approval_round: input.round,
|
|
529
|
+
answer_hash: shell.answer_hash,
|
|
530
|
+
user_answer_evidence: `answer_hash:${shell.answer_hash}`,
|
|
531
|
+
}
|
|
532
|
+
: {}),
|
|
533
|
+
});
|
|
534
|
+
intentStateChanged = true;
|
|
535
|
+
}
|
|
536
|
+
if (result.action === "noop" && !intentStateChanged) return { kind: "noop" as const };
|
|
537
|
+
state.rounds = result.rounds;
|
|
538
|
+
return { kind: "write" as const, value: { ...envelope, updated_at: now } as Record<string, unknown> };
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
if (!result) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
542
|
+
const warnings = committed.written
|
|
543
|
+
? await runDeepInterviewPostCommitEffects({
|
|
544
|
+
cwd,
|
|
545
|
+
statePath,
|
|
546
|
+
sessionId: options.sessionId,
|
|
547
|
+
envelope: committed.stamped,
|
|
548
|
+
revision: committed.revision,
|
|
549
|
+
writer: "deep-interview-recorder",
|
|
550
|
+
auditWarnings: committed.warnings,
|
|
551
|
+
})
|
|
552
|
+
: [];
|
|
553
|
+
return { action: result.action, record: result.record, warnings };
|
|
519
554
|
}
|
|
520
555
|
|
|
521
556
|
/**
|
|
@@ -529,21 +564,58 @@ export async function appendOrMergeDeepInterviewRound(
|
|
|
529
564
|
* not finite, that comparison is treated as non-matching, so no prior is selected
|
|
530
565
|
* rather than risking a spurious comparison against an unrelated round.
|
|
531
566
|
*/
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
567
|
+
function legacyScoringRoundResult(
|
|
568
|
+
envelope: DeepInterviewStateEnvelope,
|
|
569
|
+
input: DeepInterviewScoringInput,
|
|
570
|
+
): DeepInterviewRoundResultV1 {
|
|
571
|
+
if (input.roundResult) return input.roundResult;
|
|
572
|
+
const state = envelope.state as Record<string, unknown>;
|
|
573
|
+
const type = state.type;
|
|
574
|
+
const dimensions =
|
|
575
|
+
type === "brownfield"
|
|
576
|
+
? ["goal", "constraints", "criteria", "context"]
|
|
577
|
+
: type === "greenfield"
|
|
578
|
+
? ["goal", "constraints", "criteria"]
|
|
579
|
+
: undefined;
|
|
580
|
+
if (
|
|
581
|
+
!dimensions?.every(
|
|
582
|
+
dimension => typeof input.scores[dimension] === "number" && Number.isFinite(input.scores[dimension]),
|
|
583
|
+
)
|
|
584
|
+
) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
"DI_LEGACY_SCORING_INPUT_INSUFFICIENT: provide roundResult with complete global and component scores",
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const topology = state.topology;
|
|
590
|
+
const hasActiveComponents =
|
|
591
|
+
!!topology &&
|
|
592
|
+
typeof topology === "object" &&
|
|
593
|
+
!Array.isArray(topology) &&
|
|
594
|
+
(() => {
|
|
595
|
+
const topologyRecord = topology as Record<string, unknown>;
|
|
596
|
+
if (!Array.isArray(topologyRecord.components)) return false;
|
|
597
|
+
const components = topologyRecord.components as unknown[];
|
|
598
|
+
return components.some(
|
|
599
|
+
(component: unknown) =>
|
|
600
|
+
component &&
|
|
601
|
+
typeof component === "object" &&
|
|
602
|
+
!Array.isArray(component) &&
|
|
603
|
+
(component as Record<string, unknown>).active !== false &&
|
|
604
|
+
(component as Record<string, unknown>).status !== "deferred",
|
|
605
|
+
);
|
|
606
|
+
})();
|
|
607
|
+
if (hasActiveComponents) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
"DI_LEGACY_SCORING_INPUT_INSUFFICIENT: provide roundResult with complete global and component scores",
|
|
610
|
+
);
|
|
545
611
|
}
|
|
546
|
-
return
|
|
612
|
+
return {
|
|
613
|
+
global_scores: Object.fromEntries(
|
|
614
|
+
dimensions.map(dimension => [dimension, input.scores[dimension]]),
|
|
615
|
+
) as DeepInterviewRoundResultV1["global_scores"],
|
|
616
|
+
component_scores: {},
|
|
617
|
+
...(input.triggers === undefined ? {} : { triggers: input.triggers }),
|
|
618
|
+
};
|
|
547
619
|
}
|
|
548
620
|
|
|
549
621
|
/** Merge scoring output into the same round record, transitioning to `scored`. */
|
|
@@ -551,53 +623,64 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
551
623
|
cwd: string,
|
|
552
624
|
statePath: string,
|
|
553
625
|
input: DeepInterviewScoringInput,
|
|
554
|
-
options:
|
|
555
|
-
): Promise<{ record: DeepInterviewRoundRecord }> {
|
|
626
|
+
options: RecorderMutationOptions = {},
|
|
627
|
+
): Promise<{ record: DeepInterviewRoundRecord; warnings: DeepInterviewPostCommitWarning[] }> {
|
|
556
628
|
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
629
|
+
if (!options.sessionId) throw new Error("deep-interview recorder requires a session id");
|
|
630
|
+
const initial = await readEnvelope(statePath);
|
|
631
|
+
const now = new Date().toISOString();
|
|
632
|
+
let record: DeepInterviewRoundRecord | undefined;
|
|
633
|
+
const committed = await transformGuardedWorkflowEnvelopeAtomic(statePath, {
|
|
634
|
+
cwd,
|
|
635
|
+
expectedRevision: options.expectedRevision ?? existingStateRevision(initial) ?? 0,
|
|
636
|
+
receipt: {
|
|
637
|
+
cwd,
|
|
638
|
+
skill: "deep-interview",
|
|
639
|
+
owner: "gjc-runtime",
|
|
640
|
+
command: "gjc deep-interview score-round",
|
|
641
|
+
sessionId: options.sessionId,
|
|
642
|
+
nowIso: now,
|
|
643
|
+
},
|
|
644
|
+
audit: {
|
|
645
|
+
category: "state",
|
|
646
|
+
verb: "write",
|
|
647
|
+
owner: "gjc-runtime",
|
|
648
|
+
skill: "deep-interview",
|
|
649
|
+
sessionId: options.sessionId,
|
|
650
|
+
},
|
|
651
|
+
transform: current => {
|
|
652
|
+
const envelope = ensureDeepInterviewStateShape(current);
|
|
653
|
+
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
654
|
+
const roundKey = deriveRoundKey(interviewId, input);
|
|
655
|
+
const outcome = applyDeepInterviewRoundResultV1(
|
|
656
|
+
envelope,
|
|
657
|
+
roundKey,
|
|
658
|
+
legacyScoringRoundResult(envelope, input),
|
|
659
|
+
now,
|
|
660
|
+
);
|
|
661
|
+
const next = ensureDeepInterviewStateShape(outcome.envelope);
|
|
662
|
+
record = readRounds(next).find(round => round.round_key === roundKey);
|
|
663
|
+
if (!record) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
664
|
+
if (outcome.kind === "noop") return { kind: "noop" as const };
|
|
665
|
+
return {
|
|
666
|
+
kind: "write" as const,
|
|
667
|
+
value: { ...next, schema_version: 1, updated_at: now } as Record<string, unknown>,
|
|
668
|
+
};
|
|
669
|
+
},
|
|
563
670
|
});
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
ambiguity: clampResult.effective,
|
|
578
|
-
reported_ambiguity: input.ambiguity,
|
|
579
|
-
ambiguity_floor: breakdown.floor,
|
|
580
|
-
};
|
|
581
|
-
nextRounds = enrichedRounds.map(item => (item.round_key === record.round_key ? record : item));
|
|
582
|
-
}
|
|
583
|
-
// Fail closed: a scored transition that violates the bidirectional invariant
|
|
584
|
-
// (an active trigger that improves the affected dimension or fails to raise
|
|
585
|
-
// overall ambiguity, or a disputed/unresolved trigger lacking a rationale) must
|
|
586
|
-
// never be persisted — storing it lets the interview falsely converge. Validate
|
|
587
|
-
// the effective (floor-clamped) record against the most recent prior scored round.
|
|
588
|
-
const prior = latestPriorScoredRound(rounds, record.round_key, record.round);
|
|
589
|
-
const validation = validateDeepInterviewScoredTransition(prior, record);
|
|
590
|
-
if (!validation.ok) {
|
|
591
|
-
throw new Error(
|
|
592
|
-
`deep-interview scored transition for round ${record.round} is invalid and was refused: ${validation.violations.join("; ")}`,
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
inner.rounds = nextRounds;
|
|
596
|
-
inner.current_ambiguity = clampResult.effective;
|
|
597
|
-
inner.ambiguity_floor = breakdown;
|
|
598
|
-
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview score-round");
|
|
599
|
-
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
600
|
-
return { record };
|
|
671
|
+
if (!record) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
672
|
+
const warnings = committed.written
|
|
673
|
+
? await runDeepInterviewPostCommitEffects({
|
|
674
|
+
cwd,
|
|
675
|
+
statePath,
|
|
676
|
+
sessionId: options.sessionId,
|
|
677
|
+
envelope: committed.stamped,
|
|
678
|
+
revision: committed.revision,
|
|
679
|
+
writer: "deep-interview-recorder",
|
|
680
|
+
auditWarnings: committed.warnings,
|
|
681
|
+
})
|
|
682
|
+
: [];
|
|
683
|
+
return { record, warnings };
|
|
601
684
|
}
|
|
602
685
|
|
|
603
686
|
/** Compact projection so callers read a slice instead of the full transcript. */
|