@gajae-code/coding-agent 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -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/notify-cli.d.ts +7 -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.d.ts +8 -0
- 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 +12 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/main.d.ts +4 -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/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/notifications/config.d.ts +8 -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 +15 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tools/computer.d.ts +2 -2
- 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/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +37 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/notify-cli.ts +91 -7
- package/src/cli.ts +3 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- 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.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
- package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/extensibility/extensions/types.ts +9 -0
- 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 +13 -0
- package/src/gjc-runtime/session-state-sidecar.ts +59 -11
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/team-runtime.ts +23 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
- package/src/hooks/native-skill-hook.ts +29 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +52 -41
- 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/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +179 -17
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +43 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/notifications/config.ts +9 -1
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +39 -9
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +3 -1
- package/src/notifications/telegram-daemon.ts +217 -43
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/computer.md +2 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +20 -6
- package/src/session/agent-session.ts +225 -31
- package/src/session/agent-storage.ts +4 -0
- package/src/session/session-manager.ts +44 -2
- package/src/slash-commands/builtin-registry.ts +153 -71
- package/src/task/executor.ts +7 -4
- package/src/task/index.ts +9 -3
- package/src/task/output-manager.ts +16 -6
- package/src/task/receipt.ts +26 -7
- package/src/tools/computer.ts +246 -43
- package/src/tools/telegram-send.ts +6 -0
- 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
|
@@ -923,10 +923,22 @@ function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
|
|
|
923
923
|
}
|
|
924
924
|
|
|
925
925
|
function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
|
|
926
|
-
if (model.
|
|
926
|
+
if (model.contextWindow !== undefined) {
|
|
927
927
|
return model;
|
|
928
928
|
}
|
|
929
|
-
|
|
929
|
+
if (model.id === "gpt-5.4" && model.provider !== "github-copilot") {
|
|
930
|
+
return { ...model, contextWindow: 1_000_000 };
|
|
931
|
+
}
|
|
932
|
+
// Custom GPT-5.5 endpoints that are not first-party `openai-responses` are
|
|
933
|
+
// typically Codex passthrough proxies (e.g. CLIProxyAPI fronting
|
|
934
|
+
// chatgpt.com/backend-api/codex), where the request path enforces the 272K
|
|
935
|
+
// prompt budget even though the model advertises a 1M total window.
|
|
936
|
+
// Without this default the bundled reference resolves to 1M, compaction
|
|
937
|
+
// never fires in time, and requests die with context_length_exceeded.
|
|
938
|
+
if (model.id === "gpt-5.5" && model.api !== "openai-responses") {
|
|
939
|
+
return { ...model, contextWindow: 272_000 };
|
|
940
|
+
}
|
|
941
|
+
return model;
|
|
930
942
|
}
|
|
931
943
|
|
|
932
944
|
function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
|
package/src/config/settings.ts
CHANGED
|
@@ -475,6 +475,28 @@ export class Settings {
|
|
|
475
475
|
this.override("modelRoles", { ...shallowStringRecord(runtimeOverrides), [role]: modelId });
|
|
476
476
|
}
|
|
477
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Set an agent model override while keeping any live runtime override aligned.
|
|
480
|
+
*
|
|
481
|
+
* Runtime model profiles override `task.agentModelOverrides` for the current
|
|
482
|
+
* session. A user-selected role assignment must win immediately in that same
|
|
483
|
+
* session, but only the explicit agent change should be persisted.
|
|
484
|
+
*/
|
|
485
|
+
setAgentModelOverride(agentName: string, modelId: string): void {
|
|
486
|
+
const current = shallowStringRecord(getByPath(this.#global, ["task", "agentModelOverrides"]));
|
|
487
|
+
const runtimeOverrides = getByPath(this.#overrides, ["task", "agentModelOverrides"]);
|
|
488
|
+
const updateRuntimeOverride =
|
|
489
|
+
!!runtimeOverrides && typeof runtimeOverrides === "object" && !Array.isArray(runtimeOverrides);
|
|
490
|
+
|
|
491
|
+
this.set("task.agentModelOverrides", { ...current, [agentName]: modelId });
|
|
492
|
+
|
|
493
|
+
if (updateRuntimeOverride) {
|
|
494
|
+
this.override("task.agentModelOverrides", {
|
|
495
|
+
...shallowStringRecord(runtimeOverrides),
|
|
496
|
+
[agentName]: modelId,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
478
500
|
|
|
479
501
|
/**
|
|
480
502
|
* Get a model role (helper for modelRoles record).
|
|
@@ -175,6 +175,7 @@ type CoordinatorEventKind =
|
|
|
175
175
|
| "turn.queued"
|
|
176
176
|
| "turn.delivering"
|
|
177
177
|
| "turn.active"
|
|
178
|
+
| "turn.acknowledged"
|
|
178
179
|
| "turn.waiting_for_answer"
|
|
179
180
|
| "turn.completed"
|
|
180
181
|
| "turn.failed"
|
|
@@ -1121,6 +1122,18 @@ async function markTurnAcknowledgedFromRuntimeState(
|
|
|
1121
1122
|
};
|
|
1122
1123
|
await writeTurnRecord(namespaceDir, acknowledged);
|
|
1123
1124
|
await writeActiveTurn(namespaceDir, acknowledged);
|
|
1125
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
1126
|
+
kind: "turn.acknowledged",
|
|
1127
|
+
sessionId: acknowledged.session_id,
|
|
1128
|
+
turnId: acknowledged.turn_id,
|
|
1129
|
+
summary: `Turn ${acknowledged.turn_id} was acknowledged by the GJC runtime`,
|
|
1130
|
+
payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, acknowledged.turn_id)),
|
|
1131
|
+
metadata: {
|
|
1132
|
+
status: acknowledged.status,
|
|
1133
|
+
tmux_keys_sent: acknowledged.delivery.tmux_keys_sent ?? null,
|
|
1134
|
+
prompt_acknowledged: true,
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1124
1137
|
return acknowledged;
|
|
1125
1138
|
}
|
|
1126
1139
|
|
|
@@ -1533,14 +1546,27 @@ async function waitForCoordinatorEvents(namespaceDir: string, timeoutMs: number)
|
|
|
1533
1546
|
};
|
|
1534
1547
|
const timer = setTimeout(finish, Math.max(timeoutMs, 0));
|
|
1535
1548
|
timer.unref?.();
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1549
|
+
const eventDir = eventsDir(namespaceDir);
|
|
1550
|
+
const watchedDirs = [
|
|
1551
|
+
eventDir,
|
|
1552
|
+
turnsDir(namespaceDir),
|
|
1553
|
+
path.join(namespaceDir, "active-turns"),
|
|
1554
|
+
path.join(namespaceDir, "session-states"),
|
|
1555
|
+
];
|
|
1556
|
+
for (const dir of watchedDirs) {
|
|
1557
|
+
await ensureDir(dir);
|
|
1558
|
+
try {
|
|
1559
|
+
const watcher = nodeFs.watch(dir, (_eventType, filename) => {
|
|
1560
|
+
if (dir === eventDir) {
|
|
1561
|
+
if (filename === "event-journal.jsonl" || filename === "latest-seq.json") finish();
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (typeof filename === "string" && filename.endsWith(".json")) finish();
|
|
1565
|
+
});
|
|
1566
|
+
watchers.push(watcher);
|
|
1567
|
+
} catch {
|
|
1568
|
+
// Directory may not be watchable on this platform; the timeout remains a bounded fallback.
|
|
1569
|
+
}
|
|
1544
1570
|
}
|
|
1545
1571
|
return deferred.promise;
|
|
1546
1572
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: deep-interview
|
|
3
3
|
description: Socratic deep interview with mathematical ambiguity gating before explicit execution approval
|
|
4
|
-
argument-hint: "[--quick|--standard|--deep] <idea or vague description>"
|
|
4
|
+
argument-hint: "[--trace] [--quick|--standard|--deep] <idea or vague description>"
|
|
5
5
|
pipeline: [deep-interview, plan]
|
|
6
6
|
handoff-policy: approval-required
|
|
7
7
|
handoff: .gjc/_session-{sessionid}/specs/deep-interview-{slug}.md
|
|
@@ -20,12 +20,15 @@ Deep Interview implements Ouroboros-inspired Socratic questioning with mathemati
|
|
|
20
20
|
- User says "ouroboros", "socratic", "I have a vague idea", "not sure exactly what I want"
|
|
21
21
|
- User wants to avoid "that's not what I meant" outcomes from autonomous execution
|
|
22
22
|
- Task is complex enough that jumping to code would waste cycles on scope discovery
|
|
23
|
+
- User asks for implementation but the target, scope, acceptance criteria, or safety boundary is ambiguous enough that mutation would require guessing
|
|
23
24
|
- User wants mathematically-validated clarity before committing to execution
|
|
24
25
|
- User explicitly requests deep-interview even after being told the request is already clear, bounded, and low-risk
|
|
26
|
+
- User requests a trace/research pre-step before the interview, e.g. `/skill:deep-interview --trace <idea>`
|
|
25
27
|
</Use_When>
|
|
26
28
|
|
|
27
29
|
<Do_Not_Use_When>
|
|
28
30
|
- User has a detailed, specific request with file paths, function names, or acceptance criteria -- execute directly
|
|
31
|
+
- User has an explicit concrete low-risk implementation request with enough target, scope, and acceptance criteria to execute safely -- execute directly
|
|
29
32
|
- User wants to explore options or brainstorm -- use `ralplan` skill instead
|
|
30
33
|
- User wants a quick fix or single change -- use direct execution, not deep-interview or role-agent delegation
|
|
31
34
|
- User says "just do it" or "skip the questions" without an explicit execution path -- respect their intent by exiting deep-interview, not by writing a `pending approval` spec
|
|
@@ -51,6 +54,7 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
|
|
|
51
54
|
- When the locked topology has multiple active components, score and target each component explicitly so depth-first clarity on one component cannot hide ambiguity in siblings
|
|
52
55
|
- Keep prompt payloads budgeted: summarize or trim oversized initial context/history before composing question, scoring, spec, or handoff prompts
|
|
53
56
|
- If the user's initial context is oversized, create a concise prompt-safe summary first and wait for that summary before ambiguity scoring, question generation, or downstream execution handoff
|
|
57
|
+
- Route ambiguous implementation asks to clarification, deep-interview, or downstream `ralplan` before mutation; do not infer missing target, scope, acceptance criteria, or safety boundary just to start coding.
|
|
54
58
|
- Do not proceed to execution until ambiguity ≤ the resolved threshold for this run and the user explicitly approves a scoped execution path
|
|
55
59
|
- Treat user wording such as `implementation`, "implementation plan", Korean `구현`, or "구현 계획" as describing the eventual target, not permission to implement now.
|
|
56
60
|
- While still in deep-interview, do not implement, edit/write code, launch implementation workers, or start task/skill/ultragoal implementation; continue interviewing for scope, risks, acceptance criteria, and unknowns.
|
|
@@ -62,6 +66,7 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
|
|
|
62
66
|
- Refine free-text answers into a structured interpretation and confirm nothing is lost before scoring
|
|
63
67
|
- After 3 consecutive agent-resolved answers (accepted auto-research candidates or auto-answers), route the next question to the user (dialectic rhythm guard)
|
|
64
68
|
- Run an independent closure audit and a one-sentence goal restatement, each requiring explicit user confirmation, before crystallizing the spec
|
|
69
|
+
- When `--trace` is active, use the bounded trace evidence summary as pre-question context; never dump raw logs, raw files, or unbounded search output into questions, scoring, specs, or handoffs
|
|
65
70
|
</Execution_Policy>
|
|
66
71
|
|
|
67
72
|
<Internal_Auto_Mode_Protocol>
|
|
@@ -133,6 +138,16 @@ If `{{ARGUMENTS}}` is already clear, bounded, low-risk, and asks for a quick fix
|
|
|
133
138
|
|
|
134
139
|
This gate exists to prevent deep-interview from making easy problems harder. A small verification need does not make a request interview-worthy.
|
|
135
140
|
|
|
141
|
+
## Phase 0.75: Optional Trace Pre-Step
|
|
142
|
+
|
|
143
|
+
Run this phase only when the active deep-interview state or invocation indicates `--trace` / `state.trace.enabled === true`. It is a pre-interview research step, not an implementation phase.
|
|
144
|
+
|
|
145
|
+
1. Read the native trace summary from active deep-interview state (`trace`, `state.trace`, or `state.trace_summary`). The native seed must have produced this summary before any interview question.
|
|
146
|
+
2. Treat the summary as compact evidence: project hints, relevant paths, and path-level findings only. Do not expand it by dumping raw files, raw logs, or unbounded command output.
|
|
147
|
+
3. Store or preserve it under `state.trace_summary` and fold it into `codebase_context` with citations to the summarized paths.
|
|
148
|
+
4. Use trace findings to influence Round 0 topology, Phase 2 question targeting, requirement wording, acceptance criteria, and final Technical Context. Normal no-trace interviews must behave exactly as before.
|
|
149
|
+
5. If `--trace` was requested but no valid bounded summary exists, increment `architect_failures` or record an internal audit note, then continue with the normal no-trace path without surfacing tool noise.
|
|
150
|
+
|
|
136
151
|
## Phase 1: Initialize
|
|
137
152
|
|
|
138
153
|
1. **Parse the user's idea** from `{{ARGUMENTS}}`
|
|
@@ -177,6 +192,7 @@ This gate exists to prevent deep-interview from making easy problems harder. A s
|
|
|
177
192
|
"threshold": <resolvedThreshold>,
|
|
178
193
|
"threshold_source": "<resolvedThresholdSource>",
|
|
179
194
|
"language": "<existing language object from active state, if present>",
|
|
195
|
+
"trace_summary": "<bounded trace summary when --trace is active, else null>",
|
|
180
196
|
"codebase_context": null,
|
|
181
197
|
"topology": {
|
|
182
198
|
"status": "pending|confirmed|legacy_missing",
|
|
@@ -222,6 +238,7 @@ Run this gate exactly once after Phase 1 initialization and before any Phase 2 a
|
|
|
222
238
|
- Extract top-level verbs/nouns, workstreams, surfaces, integrations, or deliverables that can succeed or fail independently.
|
|
223
239
|
- Prefer 1-6 components. If more than 6 candidates appear, group siblings at the highest useful level and note the grouping rationale.
|
|
224
240
|
- Do not treat implementation tasks, fields, or sub-features as top-level components unless the user framed them as independent outcomes.
|
|
241
|
+
- When `--trace` is active, include trace-summarized paths as topology evidence, but do not add implementation sub-tasks as top-level components solely because trace found files.
|
|
225
242
|
2. **Ask one confirmation question** before Round 1:
|
|
226
243
|
|
|
227
244
|
```
|
|
@@ -289,6 +306,7 @@ Build the question generation prompt with:
|
|
|
289
306
|
- Current clarity scores per dimension (which is weakest?)
|
|
290
307
|
- Lateral-review panel findings (if convened this round -- see Phase 3)
|
|
291
308
|
- Brownfield codebase context (if applicable), summarized to cited paths/symbols/patterns instead of raw dumps
|
|
309
|
+
- Bounded trace summary (when `--trace` is active): project hints, relevant paths, and findings only; cite paths instead of raw content
|
|
292
310
|
- Locked topology from Round 0, including active components, deferred components, prior per-component scores, and `last_targeted_component_id`
|
|
293
311
|
|
|
294
312
|
- `language` from active state when present; apply `language.instruction` to all natural-language user-facing question text, rationale, and options
|
|
@@ -370,11 +388,22 @@ Ambiguity-raising triggers:
|
|
|
370
388
|
|
|
371
389
|
Use **mechanism A** for every ambiguity rise: a trigger LOWERS the affected component/dimension clarity score, and the existing weighted formula raises ambiguity. There is **no separate penalty term**; ambiguity remains bounded by the same greenfield/brownfield formula.
|
|
372
390
|
|
|
391
|
+
**Deterministic ambiguity floor (runtime-enforced).** The runtime independently computes a code-level floor from persisted state and clamps every reported ambiguity to `max(reported, floor)` at write time — the scorer cannot under-report below what code can objectively measure:
|
|
392
|
+
|
|
393
|
+
- `+0.10` per established fact marked `disputed` that has no `superseded_by` resolution (contradiction pressure)
|
|
394
|
+
- `+0.05` per active topology component whose goal/constraints/criteria clarity is still unscored (gap pressure — persist `topology.components[].clarity_scores` every round or the floor blocks convergence)
|
|
395
|
+
- `+0.05 × (auto-answered rounds / scored rounds)` (assumption dilution)
|
|
396
|
+
|
|
397
|
+
Cooperate with the floor rather than fight it:
|
|
398
|
+
- Replacing an already-scored answer for the same round (a retraction/pivot) automatically marks that round's established facts as disputed; ambiguity rises mechanically even when no trigger is reported. Treat a floor-driven rise as trigger evidence and score the affected dimensions accordingly.
|
|
399
|
+
- A disputed fact keeps the floor at or above `0.10` — above the default threshold — so convergence is blocked until the dispute is resolved: either the user re-confirms the original fact (set `disputed: false`) or the superseding decision is recorded as a new established fact and the old fact gets `superseded_by: <new fact id>`. Never delete the contradicted fact.
|
|
400
|
+
- When the effective score was clamped upward, the persisted round carries `reported_ambiguity` (your raw score) and `ambiguity_floor`; report the floor and its dominant cause in the Step 2d table instead of pretending the raw score held.
|
|
401
|
+
|
|
373
402
|
The rise is SILENT: no modal, no forced-resolution step, and no dedicated conflict UI. Surface it through the normal per-round report and by targeting the next question at the affected component/dimension.
|
|
374
403
|
|
|
375
404
|
Structured scorer output is required. Include `triggers`, `trigger_status`, `affected_component`, `affected_dimension`, `prior_dimension_score`, `new_dimension_score`, `prior_ambiguity`, `new_ambiguity`, `evidence`, `contradicted_established_fact` when relevant, and `disputed_unresolved_rationale` when applicable.
|
|
376
405
|
|
|
377
|
-
Established-facts maintenance: promote stable confirmed decisions into `state.established_facts` with source/evidence; when a new answer contradicts an established fact, mark the fact disputed and preserve the contradicted fact instead of deleting it.
|
|
406
|
+
Established-facts maintenance: promote stable confirmed decisions into `state.established_facts` with source/evidence; when a new answer contradicts an established fact, mark the fact disputed and preserve the contradicted fact instead of deleting it. When the user later confirms the new direction, record the superseding decision as a new established fact and set `superseded_by: <new fact id>` on the disputed fact — that is the only way to release the deterministic floor pressure while keeping the audit trail.
|
|
378
407
|
|
|
379
408
|
TRANSITION VALIDATION: if a trigger is present, the affected dimension must not improve and overall ambiguity must rise vs the prior scored round, unless the trigger is explicitly marked disputed or unresolved with rationale.
|
|
380
409
|
|
|
@@ -396,6 +425,9 @@ Locked topology:
|
|
|
396
425
|
Established facts:
|
|
397
426
|
{state.established_facts}
|
|
398
427
|
|
|
428
|
+
Trace summary:
|
|
429
|
+
{state.trace_summary if --trace was active, else "not requested"}
|
|
430
|
+
|
|
399
431
|
Score each active component on each dimension, then provide the overall dimension scores as the minimum or coverage-weighted weakest score across active components. Deferred components are excluded from ambiguity math but must remain listed in topology and the final spec.
|
|
400
432
|
|
|
401
433
|
Score each dimension:
|
|
@@ -465,6 +497,7 @@ Round {n} complete.
|
|
|
465
497
|
| Success Criteria | {s} | {w} | {s*w} | {gap or "Clear"} |
|
|
466
498
|
| Context (brownfield) | {s} | {w} | {s*w} | {gap or "Clear"} |
|
|
467
499
|
| **Ambiguity** | | | **{prior_score}% -> {score}% {up|down|flat}** | {if up: trigger name such as "A direct contradiction"} |
|
|
500
|
+
| **Floor** (only when clamped) | | | **{floor}%** | {dominant cause: disputed fact / unscored component / auto-answer dilution} |
|
|
468
501
|
|
|
469
502
|
**Topology:** Targeted {target_component_name} | Active: {active_component_count} | Deferred: {deferred_component_count} | Next rotation after: {last_targeted_component_id}
|
|
470
503
|
|
|
@@ -525,7 +558,7 @@ When ambiguity ≤ threshold (or hard cap / early exit):
|
|
|
525
558
|
|
|
526
559
|
**Before generating the spec, two gates must pass, in order:**
|
|
527
560
|
|
|
528
|
-
**4a. Closure / Acceptance Guard.** Even when ambiguity ≤ threshold, do not treat the math as completion. Run an independent readiness audit from the full main-session perspective (including explore findings, established facts, and triggers the scorer may not have fully weighed). Confirm every active topology component has goal/constraint/criteria coverage, no unresolved or disputed trigger remains on a path that matters, and no low-confidence auto-answer is standing in for user-confirmed truth above the clarity cap. If a material gap exists, explicitly override the gate to the user — "The math says ready, but I am not accepting it yet because {gap}" — and ask the single highest-impact follow-up, returning to Phase 2. Record any override in `state.closure_overrides`.
|
|
561
|
+
**4a. Closure / Acceptance Guard.** Even when ambiguity ≤ threshold, do not treat the math as completion. Run an independent readiness audit from the full main-session perspective (including explore findings, established facts, and triggers the scorer may not have fully weighed). Confirm every active topology component has goal/constraint/criteria coverage, no unresolved or disputed trigger remains on a path that matters, no disputed established fact lacks a `superseded_by` resolution, and no low-confidence auto-answer is standing in for user-confirmed truth above the clarity cap. If a material gap exists, explicitly override the gate to the user — "The math says ready, but I am not accepting it yet because {gap}" — and ask the single highest-impact follow-up, returning to Phase 2. Record any override in `state.closure_overrides`.
|
|
529
562
|
|
|
530
563
|
**4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask`: "If someone read only this line, would they reach the same outcome you have in mind?" Offer **Yes, crystallize**, **Adjust wording**, and **Missing scope**, plus free-text, applying `language.instruction` when present. Because this gate has options, it MUST go through `ask`: do not print the Restate question and options as assistant prose with `Question:`/`Options:` labels. If the Restate gate was already printed that way, immediately call `ask` with the same question/options before accepting or waiting for any answer. On "Adjust wording" / "Missing scope", collect the exact correction with one follow-up `ask`, route it back through Step 2c scoring and established-facts maintenance (a correction can change ambiguity), then re-run closure and ask the Restate gate again. Cap at two loops; if alignment is not reached, return to Phase 2 with a targeted question instead of forcing a goal line. Persist the confirmed line as `state.restated_goal`.
|
|
531
564
|
|
|
@@ -615,8 +648,8 @@ Spec structure:
|
|
|
615
648
|
| {assumption} | {how it was questioned} | {what was decided} |
|
|
616
649
|
|
|
617
650
|
## Technical Context
|
|
618
|
-
{brownfield: relevant codebase findings from focused repo inspection
|
|
619
|
-
{greenfield: technology choices and constraints}
|
|
651
|
+
{brownfield: relevant codebase findings from focused repo inspection, canonical role-agent fact-finding, and bounded trace summary when --trace was active}
|
|
652
|
+
{greenfield: technology choices and constraints, plus bounded trace findings when --trace was active and relevant}
|
|
620
653
|
|
|
621
654
|
## Ontology (Key Entities)
|
|
622
655
|
{Fill from the FINAL round's ontology extraction, not just crystallization-time generation}
|
|
@@ -70,19 +70,22 @@ The consensus workflow:
|
|
|
70
70
|
- If only one viable option remains, explicit invalidation rationale for alternatives
|
|
71
71
|
- Deliberate mode only: pre-mortem (3 scenarios) + expanded test plan (unit/integration/e2e/observability)
|
|
72
72
|
2. **User feedback** *(--interactive only)*: If `--interactive` is set, use the `ask` tool to present the draft plan **plus the Principles / Drivers / Options summary** before review (Proceed to review / Request changes / Skip review). Otherwise, automatically proceed to review.
|
|
73
|
-
3. **
|
|
73
|
+
3. **Review fan-out after Planner persistence**: after the Planner stage is persisted, launch fresh Architect and Critic review lanes against the same immutable Planner receipt/path/sha/stage_n when the Critic lane is **plan-only** and does not consume Architect output.
|
|
74
|
+
- **Architect lane**: Architect reviews for architectural soundness and must provide the strongest steelman antithesis, at least one real tradeoff tension, and (when possible) synthesis. In deliberate mode, Architect should explicitly flag principle violations.
|
|
74
75
|
- The Architect agent/subagent must persist its review with `gjc ralplan --write --stage architect --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus compact verdict/status (`CLEAR`/`WATCH`/`BLOCK`, `APPROVE`/`COMMENT`/`REQUEST CHANGES`) instead of pasting the full review body.
|
|
75
|
-
|
|
76
|
+
- **Plan-only Critic lane**: Critic evaluates the Planner artifact against quality criteria independently of Architect output. It must enforce principle-option consistency, fair alternatives, risk mitigation clarity, testable acceptance criteria, and concrete verification steps. In deliberate mode, Critic must reject missing/weak pre-mortem or expanded test plan.
|
|
76
77
|
- The Critic agent/subagent must persist its evaluation with `gjc ralplan --write --stage critic --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus compact verdict/status (`OKAY`/`ITERATE`/`REJECT`) instead of pasting the full evaluation body.
|
|
77
|
-
|
|
78
|
+
- **Sequential fallback**: when Critic must evaluate Architect findings, verdict, antithesis, tradeoffs, synthesis, architectural status, or any other Architect-produced artifact, await the Architect result before issuing that Architect-dependent Critic pass.
|
|
79
|
+
4. **Review join gate**: before consensus, revision, reconciliation, finalization, or approval, verify both Architect and Critic receipts/verdicts exist for the same Planner artifact/pass (`path`, `sha256`, `stage_n`). A non-`CLEAR` Architect verdict, non-`APPROVE` Architect decision, or any non-`OKAY` Critic verdict routes back to Planner revision; do not finalize from only one review lane.
|
|
80
|
+
5. **Re-review loop** (max 5 iterations): Any non-`OKAY` Critic verdict (`ITERATE` or `REJECT`) or Architect result that is not `CLEAR`/`APPROVE` MUST run the same full closed loop:
|
|
78
81
|
a. Collect Architect + Critic feedback
|
|
79
82
|
b. Revise the plan by resuming the SAME persisted Planner subagent with consolidated Architect + Critic feedback (see **Persisted Planner** below); fall back to a fresh Planner spawn only per the fallback routing table
|
|
80
|
-
c. Return to
|
|
83
|
+
c. Return to the review fan-out or sequential fallback path above
|
|
81
84
|
- Persist each Planner revision with `gjc ralplan --write --stage revision --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json` before re-review, then pass the receipt/path forward instead of duplicating the full revision markdown in the parent conversation.
|
|
82
|
-
d.
|
|
83
|
-
e. Repeat this loop until Critic returns `OKAY` or 5 iterations are reached
|
|
84
|
-
f. If 5 iterations are reached without `OKAY`, present the best version to the user
|
|
85
|
-
6. **Post-ralplan interview** (intent reconciliation gate): After Critic
|
|
85
|
+
d. Re-join Architect and Critic verdicts for the same revised Planner artifact/pass
|
|
86
|
+
e. Repeat this loop until Critic returns `OKAY` **and** Architect is `CLEAR`/`APPROVE` for the same Planner artifact/pass, or 5 iterations are reached
|
|
87
|
+
f. If 5 iterations are reached without Critic `OKAY` plus Architect `CLEAR`/`APPROVE`, present the best version to the user
|
|
88
|
+
6. **Post-ralplan interview** (intent reconciliation gate): After the review join gate has both Critic `OKAY` and Architect `CLEAR`/`APPROVE` for the same Planner artifact/pass, and before the plan is finalized, reconcile the consensus plan against the user's actual intent. The goal is to make sure ralplan did not silently bake in assumptions that conflict with what the user wants.
|
|
86
89
|
a. **Collect open items** from the run: every assumption the Planner/Architect/Critic resolved by assumption rather than by stated fact, every ambiguity flagged during review, and every decision the loop made without explicit user input. Source these from the persisted `planner`/`architect`/`critic`/`revision` stage artifacts, not from memory.
|
|
87
90
|
b. **Cross-check prior context for conflicts**: glob `.gjc/_session-{sessionid}/specs/deep-interview-*.md` and other prior specs/plans/context relevant by topic. For each, list points where the consensus plan contradicts, weakens, or expands beyond a previously crystallized decision, constraint, or non-goal. Cite the conflicting artifact and line/section.
|
|
88
91
|
c. **Reconcile with the user via the `ask` tool (always, regardless of `--interactive`)**: Never stop idle with plain-text prose after the consensus loop. Every reconciliation question MUST go through the `ask` tool with contextual options plus free-text.
|
|
@@ -90,7 +93,7 @@ The consensus workflow:
|
|
|
90
93
|
- If the plan is crystal clear (no open assumptions or prior-context conflicts), skip straight to the step 8 final-options `ask` instead of inventing filler questions.
|
|
91
94
|
- For every confirmed open item, embed the resolved outcome into the final plan under an **## Intent Reconciliation** section so the `pending approval` artifact records each decision; record any item the user explicitly defers as an open confirmation under that same section.
|
|
92
95
|
d. Persist the reconciliation with `gjc ralplan --write --stage post-interview --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus a compact status (reconciled-clean / reconciled-with-revision / open-confirmations-pending) instead of pasting the full body.
|
|
93
|
-
7. On reconciliation completion, mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT`, and do not directly edit `.gjc/_session-{sessionid}/plans`. Final plan must include ADR (Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups) and, when present, the **## Intent Reconciliation** section.
|
|
96
|
+
7. On reconciliation completion, re-check the review join gate (Critic `OKAY` plus Architect `CLEAR`/`APPROVE` for the same Planner artifact/pass), mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT`, and do not directly edit `.gjc/_session-{sessionid}/plans`. Final plan must include ADR (Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups) and, when present, the **## Intent Reconciliation** section.
|
|
94
97
|
8. **Always** present the finalized plan via the `ask` tool (regardless of `--interactive`) with `workflowGate: { stage: "ralplan", kind: "approval" }` on the final question so RPC/headless clients receive a `ralplan`/`approval` workflow gate, not a deep-interview question gate. Use these options:
|
|
95
98
|
- **Refine further** — re-run the consensus loop / request changes, then return here
|
|
96
99
|
- **Approve execution via ultragoal (Recommended)** — goal-tracked autonomous execution
|
|
@@ -108,13 +111,14 @@ The consensus workflow:
|
|
|
108
111
|
|
|
109
112
|
The skill tool then dispatches the execution skill same-turn and runs `gjc state ralplan handoff --to <team|ultragoal> --json` in-process to atomically demote ralplan, promote the callee, and sync `.gjc/_session-{sessionid}/state/skill-active-state.json`. You do not need to run the handoff verb yourself.
|
|
110
113
|
|
|
111
|
-
> **Important:**
|
|
114
|
+
> **Important:** Architect and Critic MAY run in the same parallel batch only for the plan-only Critic lane after Planner persistence. Any Architect-dependent Critic pass MUST remain sequential: await Architect before issuing Critic, then apply the same review join gate before consensus.
|
|
115
|
+
|
|
112
116
|
|
|
113
117
|
Follow the Plan skill's full documentation for consensus mode details.
|
|
114
118
|
|
|
115
119
|
### Persisted Planner (consensus loop)
|
|
116
120
|
|
|
117
|
-
The Planner is a **same-session persisted subagent**: launched detached once, awaited before
|
|
121
|
+
The Planner is a **same-session persisted subagent**: launched detached once, awaited before review fan-out, then **resumed** with consolidated Architect + Critic feedback on every re-review pass instead of being re-spawned. The Architect and Critic stay **fresh, independent spawns each pass** so their verdicts remain reproducible from their pass artifacts alone; the Critic may run in parallel only when it is plan-only and tied to the same Planner receipt/path/sha/stage_n as Architect. Do NOT modify the subagent control surface; this orchestration uses the existing `subagent` resume/steer controls only.
|
|
118
122
|
|
|
119
123
|
**Persistence boundary:** this is same-parent, active-session continuity only. Resumability depends on the manager's retained subagent resume metadata and a persistent parent session (an in-memory parent yields `resumable:false`), not just the `.gjc` run-state record. A terminal subagent whose live job record was evicted can still be resumed when its retained resume descriptor points at a saved subagent session file. After a process restart, missing resume metadata, or any unavailable/failed resume, use the fresh Planner fallback.
|
|
120
124
|
|
|
@@ -112,7 +112,7 @@ Before launching `gjc team`, require a grounded context snapshot:
|
|
|
112
112
|
- unknowns/open questions
|
|
113
113
|
- likely codebase touchpoints
|
|
114
114
|
4. If ambiguity remains high, gather brownfield facts with focused repo inspection or a canonical read-only role agent first, then run `$deep-interview --quick <task>` before team launch.
|
|
115
|
-
5. If current correctness depends on official docs, version-aware framework guidance, best practices, or external dependency behavior,
|
|
115
|
+
5. If current correctness depends on official docs, version-aware framework guidance, best practices, or external dependency behavior, gather evidence before or alongside worker launch using supported surfaces only: focused repo inspection for local facts, `planner` for broad context mapping/sequencing, `architect` for architecture or external-doc-risk assessment, and direct web/search tools when configured.
|
|
116
116
|
|
|
117
117
|
Do not start the worker pane until this gate is satisfied; if forced to proceed quickly, state explicit scope/risk limitations in the launch report.
|
|
118
118
|
|
|
@@ -205,6 +205,7 @@ export class ExtensionRunner {
|
|
|
205
205
|
private readonly cwd: string,
|
|
206
206
|
private readonly sessionManager: SessionManager,
|
|
207
207
|
private readonly modelRegistry: ModelRegistry,
|
|
208
|
+
private readonly sessionMetadata?: ExtensionContext["sessionMetadata"],
|
|
208
209
|
) {
|
|
209
210
|
this.#uiContext = noOpUIContext;
|
|
210
211
|
}
|
|
@@ -458,6 +459,7 @@ export class ExtensionRunner {
|
|
|
458
459
|
hasUI: this.hasUI(),
|
|
459
460
|
cwd: this.cwd,
|
|
460
461
|
sessionManager: this.sessionManager,
|
|
462
|
+
sessionMetadata: this.sessionMetadata,
|
|
461
463
|
modelRegistry: this.modelRegistry,
|
|
462
464
|
get model() {
|
|
463
465
|
return getModel();
|
|
@@ -285,6 +285,13 @@ export interface CompactOptions {
|
|
|
285
285
|
onError?: (error: Error) => void;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
export interface ExtensionSessionMetadata {
|
|
289
|
+
kind: "main" | "sub";
|
|
290
|
+
taskDepth: number;
|
|
291
|
+
parentTaskPrefix?: string;
|
|
292
|
+
currentAgentType?: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
288
295
|
/**
|
|
289
296
|
* Context passed to extension event handlers.
|
|
290
297
|
*/
|
|
@@ -306,6 +313,8 @@ export interface ExtensionContext {
|
|
|
306
313
|
cwd: string;
|
|
307
314
|
/** Session manager (read-only) */
|
|
308
315
|
sessionManager: ReadonlySessionManager;
|
|
316
|
+
/** Session classification supplied by the SDK for extension policy decisions. */
|
|
317
|
+
sessionMetadata?: ExtensionSessionMetadata;
|
|
309
318
|
/** Model registry for API key resolution */
|
|
310
319
|
modelRegistry: ModelRegistry;
|
|
311
320
|
/** Current model (may be undefined) */
|
|
@@ -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
|
+
}
|