@gajae-code/coding-agent 0.11.8 → 0.11.9
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 +18 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- 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 +44 -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/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +12 -2
- 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/package.json +8 -8
- package/src/cli/mcp-cli.ts +4 -21
- 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 +2 -3
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- 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/internal-urls/docs-index.generated.ts +3 -2
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/interactive-mode.ts +29 -9
- 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/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +18 -2
- package/src/session/agent-session.ts +86 -0
- package/src/session/internal/managed-session-scope.ts +206 -111
- 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/task/executor.ts +13 -5
- package/src/tools/ask.ts +1 -0
|
@@ -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";
|
|
@@ -125,11 +124,9 @@ export function buildAnswerShell(
|
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
/**
|
|
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.
|
|
127
|
+
* Append-or-merge by `round_key`. An answer is a no-op only when its complete
|
|
128
|
+
* canonical shell identity matches; a scored record is immutable evidence and
|
|
129
|
+
* cannot be replaced by a late answer.
|
|
133
130
|
*/
|
|
134
131
|
export function appendOrMergeRound(
|
|
135
132
|
rounds: readonly DeepInterviewRoundRecord[],
|
|
@@ -142,9 +139,10 @@ export function appendOrMergeRound(
|
|
|
142
139
|
return { rounds: next, action: "created", record: shell };
|
|
143
140
|
}
|
|
144
141
|
const existing = next[index];
|
|
145
|
-
if (existing
|
|
142
|
+
if (deepInterviewAnswerIdentityEqual(existing, shell) && existing.answer_hash === shell.answer_hash) {
|
|
146
143
|
return { rounds: next, action: "noop", record: existing };
|
|
147
144
|
}
|
|
145
|
+
if (existing.lifecycle === "scored") throw new Error("DI_ANSWER_LIFECYCLE_CONFLICT");
|
|
148
146
|
next[index] = shell;
|
|
149
147
|
return { rounds: next, action: "replaced", record: shell };
|
|
150
148
|
}
|
|
@@ -224,10 +222,10 @@ export function validateDeepInterviewScoredTransition(
|
|
|
224
222
|
`active trigger ${trigger.kind} did not raise ambiguity (${prior.ambiguity} -> ${next.ambiguity})`,
|
|
225
223
|
);
|
|
226
224
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
const priorDim = prior.scores?.[trigger.dimension]
|
|
230
|
-
const nextDim = next.scores?.[trigger.dimension]
|
|
225
|
+
// The runtime derives both dimension values from scored records; callers cannot
|
|
226
|
+
// provide parallel trigger metrics.
|
|
227
|
+
const priorDim = prior.scores?.[trigger.dimension];
|
|
228
|
+
const nextDim = next.scores?.[trigger.dimension];
|
|
231
229
|
if (typeof priorDim !== "number" || typeof nextDim !== "number") {
|
|
232
230
|
violations.push(
|
|
233
231
|
`active trigger ${trigger.kind} is missing dimension "${trigger.dimension}" scores to prove non-improvement`,
|
|
@@ -304,19 +302,16 @@ export function projectCompactState(value: unknown, options: { lastN?: number }
|
|
|
304
302
|
// Persistence wrappers (state-writer backed; runtime-owned)
|
|
305
303
|
// =============================================================================
|
|
306
304
|
|
|
305
|
+
interface RecorderMutationOptions {
|
|
306
|
+
sessionId?: string;
|
|
307
|
+
expectedRevision?: number;
|
|
308
|
+
}
|
|
309
|
+
|
|
307
310
|
async function readEnvelope(statePath: string): Promise<DeepInterviewStateEnvelope> {
|
|
308
311
|
const read = await readExistingStateForMutation(statePath);
|
|
309
312
|
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);
|
|
313
|
+
if (read.kind === "absent") throw new Error("DI_STATE_ABSENT");
|
|
314
|
+
throw new Error(`deep-interview state at ${statePath} is corrupt or tampered (${read.error})`);
|
|
320
315
|
}
|
|
321
316
|
|
|
322
317
|
function existingStateRevision(value: unknown): number | undefined {
|
|
@@ -329,49 +324,16 @@ function interviewIdOf(envelope: DeepInterviewStateEnvelope): string | undefined
|
|
|
329
324
|
const inner = (envelope.state ?? {}) as Record<string, unknown>;
|
|
330
325
|
return typeof inner.interview_id === "string" ? inner.interview_id : undefined;
|
|
331
326
|
}
|
|
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 });
|
|
327
|
+
interface RecorderHudSyncSlot {
|
|
328
|
+
latestCommittedRevision: number;
|
|
329
|
+
tail: Promise<void>;
|
|
365
330
|
}
|
|
366
331
|
|
|
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
|
-
*/
|
|
332
|
+
const recorderHudSyncSlots = new Map<string, RecorderHudSyncSlot>();
|
|
333
|
+
|
|
372
334
|
async function syncRecorderHud(
|
|
373
335
|
cwd: string,
|
|
374
|
-
envelope:
|
|
336
|
+
envelope: Record<string, unknown>,
|
|
375
337
|
sessionId: string | undefined,
|
|
376
338
|
): Promise<void> {
|
|
377
339
|
const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
|
|
@@ -382,26 +344,65 @@ async function syncRecorderHud(
|
|
|
382
344
|
phase,
|
|
383
345
|
sessionId,
|
|
384
346
|
source: "gjc-runtime-deep-interview-recorder",
|
|
385
|
-
hud: deriveDeepInterviewHud(
|
|
386
|
-
|
|
347
|
+
hud: deriveDeepInterviewHud(envelope, { phase }),
|
|
348
|
+
...(typeof envelope.state_revision === "number" ? { committedModeRevision: envelope.state_revision } : {}),
|
|
387
349
|
});
|
|
388
350
|
}
|
|
389
351
|
|
|
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(
|
|
352
|
+
async function syncRecorderHudAtCommittedRevision(
|
|
396
353
|
cwd: string,
|
|
397
|
-
|
|
398
|
-
sessionId: string
|
|
354
|
+
envelope: Record<string, unknown>,
|
|
355
|
+
sessionId: string,
|
|
356
|
+
revision: number,
|
|
399
357
|
): Promise<void> {
|
|
358
|
+
const key = `${cwd}\0${sessionId}`;
|
|
359
|
+
const slot = recorderHudSyncSlots.get(key) ?? { latestCommittedRevision: -1, tail: Promise.resolve() };
|
|
360
|
+
recorderHudSyncSlots.set(key, slot);
|
|
361
|
+
const sync = slot.tail
|
|
362
|
+
.catch(() => undefined)
|
|
363
|
+
.then(async () => {
|
|
364
|
+
if (revision <= slot.latestCommittedRevision) return;
|
|
365
|
+
await syncRecorderHud(cwd, { ...envelope, state_revision: revision }, sessionId);
|
|
366
|
+
slot.latestCommittedRevision = revision;
|
|
367
|
+
});
|
|
368
|
+
slot.tail = sync;
|
|
369
|
+
await sync;
|
|
370
|
+
}
|
|
371
|
+
export interface DeepInterviewPostCommitWarning {
|
|
372
|
+
code: "DI_POST_COMMIT_AUDIT_FAILED" | "DI_POST_COMMIT_ACTIVITY_FAILED" | "DI_POST_COMMIT_HUD_FAILED";
|
|
373
|
+
message: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function runDeepInterviewPostCommitEffects(options: {
|
|
377
|
+
cwd: string;
|
|
378
|
+
statePath: string;
|
|
379
|
+
sessionId: string;
|
|
380
|
+
envelope: Record<string, unknown>;
|
|
381
|
+
revision: number;
|
|
382
|
+
writer: string;
|
|
383
|
+
auditWarnings?: readonly { code: "DI_POST_COMMIT_AUDIT_FAILED"; message: string }[];
|
|
384
|
+
}): Promise<DeepInterviewPostCommitWarning[]> {
|
|
385
|
+
const warnings: DeepInterviewPostCommitWarning[] = [...(options.auditWarnings ?? [])];
|
|
400
386
|
try {
|
|
401
|
-
await
|
|
402
|
-
|
|
403
|
-
|
|
387
|
+
await writeSessionActivityMarker(options.cwd, options.sessionId, {
|
|
388
|
+
writer: options.writer,
|
|
389
|
+
path: options.statePath,
|
|
390
|
+
});
|
|
391
|
+
} catch (error) {
|
|
392
|
+
warnings.push({
|
|
393
|
+
code: "DI_POST_COMMIT_ACTIVITY_FAILED",
|
|
394
|
+
message: error instanceof Error ? error.message : String(error),
|
|
395
|
+
});
|
|
404
396
|
}
|
|
397
|
+
try {
|
|
398
|
+
await syncRecorderHudAtCommittedRevision(options.cwd, options.envelope, options.sessionId, options.revision);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
warnings.push({
|
|
401
|
+
code: "DI_POST_COMMIT_HUD_FAILED",
|
|
402
|
+
message: error instanceof Error ? error.message : String(error),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return warnings;
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
/** Refresh the best-effort HUD cache from persisted deep-interview state. */
|
|
@@ -412,110 +413,129 @@ export async function syncDeepInterviewRecorderHud(
|
|
|
412
413
|
): Promise<void> {
|
|
413
414
|
const read = await readExistingStateForMutation(statePath);
|
|
414
415
|
if (read.kind !== "valid") return;
|
|
415
|
-
|
|
416
|
+
const envelope = normalizeDeepInterviewEnvelope(read.value);
|
|
417
|
+
const revision = existingStateRevision(read.value);
|
|
418
|
+
if (revision === undefined || !sessionId) return;
|
|
419
|
+
await syncRecorderHudAtCommittedRevision(cwd, envelope, sessionId, revision);
|
|
416
420
|
}
|
|
417
421
|
|
|
418
422
|
/**
|
|
419
423
|
* Record an `answered` shell for one round (append-or-merge by durable key).
|
|
420
424
|
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* marked disputed, which raises the deterministic ambiguity floor immediately —
|
|
424
|
-
* without waiting for the LLM scorer to self-report a trigger.
|
|
425
|
+
* Replacing an already-scored answer is rejected with
|
|
426
|
+
* `DI_ANSWER_LIFECYCLE_CONFLICT`; scored evidence is immutable.
|
|
425
427
|
*/
|
|
426
428
|
export async function appendOrMergeDeepInterviewRound(
|
|
427
429
|
cwd: string,
|
|
428
430
|
statePath: string,
|
|
429
431
|
input: DeepInterviewAnswerInput,
|
|
430
|
-
options:
|
|
431
|
-
): Promise<{
|
|
432
|
+
options: RecorderMutationOptions = {},
|
|
433
|
+
): Promise<{
|
|
434
|
+
action: AppendOrMergeAction;
|
|
435
|
+
record: DeepInterviewRoundRecord;
|
|
436
|
+
warnings: DeepInterviewPostCommitWarning[];
|
|
437
|
+
}> {
|
|
432
438
|
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
433
439
|
if (input.customInput !== undefined)
|
|
434
440
|
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,
|
|
441
|
+
if (!options.sessionId) throw new Error("deep-interview recorder requires a session id");
|
|
442
|
+
const initial = await readEnvelope(statePath);
|
|
443
|
+
let result: AppendOrMergeResult | undefined;
|
|
444
|
+
const now = new Date().toISOString();
|
|
445
|
+
const committed = await transformGuardedWorkflowEnvelopeAtomic(statePath, {
|
|
446
|
+
cwd,
|
|
447
|
+
expectedRevision: options.expectedRevision ?? existingStateRevision(initial) ?? 0,
|
|
448
|
+
receipt: {
|
|
449
|
+
cwd,
|
|
450
|
+
skill: "deep-interview",
|
|
451
|
+
owner: "gjc-runtime",
|
|
452
|
+
command: "gjc deep-interview record-answer",
|
|
453
|
+
sessionId: options.sessionId,
|
|
454
|
+
nowIso: now,
|
|
455
|
+
},
|
|
456
|
+
audit: {
|
|
457
|
+
category: "state",
|
|
458
|
+
verb: "write",
|
|
459
|
+
owner: "gjc-runtime",
|
|
460
|
+
skill: "deep-interview",
|
|
461
|
+
sessionId: options.sessionId,
|
|
462
|
+
},
|
|
463
|
+
transform: current => {
|
|
464
|
+
const envelope = ensureDeepInterviewStateShape(current);
|
|
465
|
+
const shell = buildAnswerShell(
|
|
466
|
+
{
|
|
467
|
+
...input,
|
|
468
|
+
interviewId: input.interviewId ?? interviewIdOf(envelope),
|
|
469
|
+
customInput: input.intent_contract || input.intent_review ? undefined : input.customInput,
|
|
470
|
+
},
|
|
471
|
+
now,
|
|
472
|
+
);
|
|
473
|
+
result = appendOrMergeRound(readRounds(envelope), shell);
|
|
474
|
+
const state = envelope.state as Record<string, unknown>;
|
|
475
|
+
let intentStateChanged = false;
|
|
476
|
+
if (input.intent_contract) {
|
|
477
|
+
if (input.round !== 0 || input.component !== "review-topology" || input.dimension !== "topology")
|
|
478
|
+
throw new Error("intent contract requires Round 0 topology metadata");
|
|
479
|
+
const confirmed =
|
|
480
|
+
input.selectedOptions?.length === 1 &&
|
|
481
|
+
input.intent_contract.confirmation_options.includes(input.selectedOptions[0]);
|
|
482
|
+
if (confirmed) {
|
|
483
|
+
const contract = createDeepInterviewIntentManifest(input.intent_contract.items, {
|
|
484
|
+
round: 0,
|
|
484
485
|
answer_hash: shell.answer_hash,
|
|
485
|
-
|
|
486
|
+
});
|
|
487
|
+
const existingContract = state.intent_contract;
|
|
488
|
+
if (existingContract !== undefined) {
|
|
489
|
+
assertDeepInterviewIntentManifest(existingContract);
|
|
490
|
+
if (
|
|
491
|
+
existingContract.digest !== contract.digest ||
|
|
492
|
+
existingContract.confirmation_answer_hash !== contract.confirmation_answer_hash
|
|
493
|
+
)
|
|
494
|
+
throw new Error("locked intent contract cannot be replaced");
|
|
495
|
+
} else {
|
|
496
|
+
state.intent_contract = contract;
|
|
497
|
+
intentStateChanged = true;
|
|
486
498
|
}
|
|
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;
|
|
499
|
+
}
|
|
509
500
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
501
|
+
if (input.intent_review) {
|
|
502
|
+
if (input.round <= 0) throw new Error("intent review requires a post-Round-0 answer");
|
|
503
|
+
const locked = state.intent_contract;
|
|
504
|
+
assertDeepInterviewIntentManifest(locked);
|
|
505
|
+
const approved =
|
|
506
|
+
input.selectedOptions?.length === 1 &&
|
|
507
|
+
input.intent_review.approval_options.includes(input.selectedOptions[0]);
|
|
508
|
+
state.intent_review = reviewDeepInterviewIntent(locked, input.intent_review.observed_items, {
|
|
509
|
+
status: approved ? "approved" : "pending",
|
|
510
|
+
supporting_substitutions: input.intent_review.supporting_substitutions,
|
|
511
|
+
...(approved
|
|
512
|
+
? {
|
|
513
|
+
approval_round: input.round,
|
|
514
|
+
answer_hash: shell.answer_hash,
|
|
515
|
+
user_answer_evidence: `answer_hash:${shell.answer_hash}`,
|
|
516
|
+
}
|
|
517
|
+
: {}),
|
|
518
|
+
});
|
|
519
|
+
intentStateChanged = true;
|
|
520
|
+
}
|
|
521
|
+
if (result.action === "noop" && !intentStateChanged) return { kind: "noop" as const };
|
|
522
|
+
state.rounds = result.rounds;
|
|
523
|
+
return { kind: "write" as const, value: { ...envelope, updated_at: now } as Record<string, unknown> };
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
if (!result) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
527
|
+
const warnings = committed.written
|
|
528
|
+
? await runDeepInterviewPostCommitEffects({
|
|
529
|
+
cwd,
|
|
530
|
+
statePath,
|
|
531
|
+
sessionId: options.sessionId,
|
|
532
|
+
envelope: committed.stamped,
|
|
533
|
+
revision: committed.revision,
|
|
534
|
+
writer: "deep-interview-recorder",
|
|
535
|
+
auditWarnings: committed.warnings,
|
|
536
|
+
})
|
|
537
|
+
: [];
|
|
538
|
+
return { action: result.action, record: result.record, warnings };
|
|
519
539
|
}
|
|
520
540
|
|
|
521
541
|
/**
|
|
@@ -529,21 +549,58 @@ export async function appendOrMergeDeepInterviewRound(
|
|
|
529
549
|
* not finite, that comparison is treated as non-matching, so no prior is selected
|
|
530
550
|
* rather than risking a spurious comparison against an unrelated round.
|
|
531
551
|
*/
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
552
|
+
function legacyScoringRoundResult(
|
|
553
|
+
envelope: DeepInterviewStateEnvelope,
|
|
554
|
+
input: DeepInterviewScoringInput,
|
|
555
|
+
): DeepInterviewRoundResultV1 {
|
|
556
|
+
if (input.roundResult) return input.roundResult;
|
|
557
|
+
const state = envelope.state as Record<string, unknown>;
|
|
558
|
+
const type = state.type;
|
|
559
|
+
const dimensions =
|
|
560
|
+
type === "brownfield"
|
|
561
|
+
? ["goal", "constraints", "criteria", "context"]
|
|
562
|
+
: type === "greenfield"
|
|
563
|
+
? ["goal", "constraints", "criteria"]
|
|
564
|
+
: undefined;
|
|
565
|
+
if (
|
|
566
|
+
!dimensions?.every(
|
|
567
|
+
dimension => typeof input.scores[dimension] === "number" && Number.isFinite(input.scores[dimension]),
|
|
568
|
+
)
|
|
569
|
+
) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
"DI_LEGACY_SCORING_INPUT_INSUFFICIENT: provide roundResult with complete global and component scores",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const topology = state.topology;
|
|
575
|
+
const hasActiveComponents =
|
|
576
|
+
!!topology &&
|
|
577
|
+
typeof topology === "object" &&
|
|
578
|
+
!Array.isArray(topology) &&
|
|
579
|
+
(() => {
|
|
580
|
+
const topologyRecord = topology as Record<string, unknown>;
|
|
581
|
+
if (!Array.isArray(topologyRecord.components)) return false;
|
|
582
|
+
const components = topologyRecord.components as unknown[];
|
|
583
|
+
return components.some(
|
|
584
|
+
(component: unknown) =>
|
|
585
|
+
component &&
|
|
586
|
+
typeof component === "object" &&
|
|
587
|
+
!Array.isArray(component) &&
|
|
588
|
+
(component as Record<string, unknown>).active !== false &&
|
|
589
|
+
(component as Record<string, unknown>).status !== "deferred",
|
|
590
|
+
);
|
|
591
|
+
})();
|
|
592
|
+
if (hasActiveComponents) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
"DI_LEGACY_SCORING_INPUT_INSUFFICIENT: provide roundResult with complete global and component scores",
|
|
595
|
+
);
|
|
545
596
|
}
|
|
546
|
-
return
|
|
597
|
+
return {
|
|
598
|
+
global_scores: Object.fromEntries(
|
|
599
|
+
dimensions.map(dimension => [dimension, input.scores[dimension]]),
|
|
600
|
+
) as DeepInterviewRoundResultV1["global_scores"],
|
|
601
|
+
component_scores: {},
|
|
602
|
+
...(input.triggers === undefined ? {} : { triggers: input.triggers }),
|
|
603
|
+
};
|
|
547
604
|
}
|
|
548
605
|
|
|
549
606
|
/** Merge scoring output into the same round record, transitioning to `scored`. */
|
|
@@ -551,53 +608,64 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
551
608
|
cwd: string,
|
|
552
609
|
statePath: string,
|
|
553
610
|
input: DeepInterviewScoringInput,
|
|
554
|
-
options:
|
|
555
|
-
): Promise<{ record: DeepInterviewRoundRecord }> {
|
|
611
|
+
options: RecorderMutationOptions = {},
|
|
612
|
+
): Promise<{ record: DeepInterviewRoundRecord; warnings: DeepInterviewPostCommitWarning[] }> {
|
|
556
613
|
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
614
|
+
if (!options.sessionId) throw new Error("deep-interview recorder requires a session id");
|
|
615
|
+
const initial = await readEnvelope(statePath);
|
|
616
|
+
const now = new Date().toISOString();
|
|
617
|
+
let record: DeepInterviewRoundRecord | undefined;
|
|
618
|
+
const committed = await transformGuardedWorkflowEnvelopeAtomic(statePath, {
|
|
619
|
+
cwd,
|
|
620
|
+
expectedRevision: options.expectedRevision ?? existingStateRevision(initial) ?? 0,
|
|
621
|
+
receipt: {
|
|
622
|
+
cwd,
|
|
623
|
+
skill: "deep-interview",
|
|
624
|
+
owner: "gjc-runtime",
|
|
625
|
+
command: "gjc deep-interview score-round",
|
|
626
|
+
sessionId: options.sessionId,
|
|
627
|
+
nowIso: now,
|
|
628
|
+
},
|
|
629
|
+
audit: {
|
|
630
|
+
category: "state",
|
|
631
|
+
verb: "write",
|
|
632
|
+
owner: "gjc-runtime",
|
|
633
|
+
skill: "deep-interview",
|
|
634
|
+
sessionId: options.sessionId,
|
|
635
|
+
},
|
|
636
|
+
transform: current => {
|
|
637
|
+
const envelope = ensureDeepInterviewStateShape(current);
|
|
638
|
+
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
639
|
+
const roundKey = deriveRoundKey(interviewId, input);
|
|
640
|
+
const outcome = applyDeepInterviewRoundResultV1(
|
|
641
|
+
envelope,
|
|
642
|
+
roundKey,
|
|
643
|
+
legacyScoringRoundResult(envelope, input),
|
|
644
|
+
now,
|
|
645
|
+
);
|
|
646
|
+
const next = ensureDeepInterviewStateShape(outcome.envelope);
|
|
647
|
+
record = readRounds(next).find(round => round.round_key === roundKey);
|
|
648
|
+
if (!record) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
649
|
+
if (outcome.kind === "noop") return { kind: "noop" as const };
|
|
650
|
+
return {
|
|
651
|
+
kind: "write" as const,
|
|
652
|
+
value: { ...next, schema_version: 1, updated_at: now } as Record<string, unknown>,
|
|
653
|
+
};
|
|
654
|
+
},
|
|
563
655
|
});
|
|
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 };
|
|
656
|
+
if (!record) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
657
|
+
const warnings = committed.written
|
|
658
|
+
? await runDeepInterviewPostCommitEffects({
|
|
659
|
+
cwd,
|
|
660
|
+
statePath,
|
|
661
|
+
sessionId: options.sessionId,
|
|
662
|
+
envelope: committed.stamped,
|
|
663
|
+
revision: committed.revision,
|
|
664
|
+
writer: "deep-interview-recorder",
|
|
665
|
+
auditWarnings: committed.warnings,
|
|
666
|
+
})
|
|
667
|
+
: [];
|
|
668
|
+
return { record, warnings };
|
|
601
669
|
}
|
|
602
670
|
|
|
603
671
|
/** Compact projection so callers read a slice instead of the full transcript. */
|