@gajae-code/coding-agent 0.8.0 → 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 +48 -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 +18 -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/tools/skill.d.ts +1 -1
- 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 +67 -12
- 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/extensibility/skills.ts +30 -2
- 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/acp/acp-agent.ts +7 -6
- 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 +4 -4
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +186 -25
- 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 +24 -13
- 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 +231 -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/skill.ts +1 -1
- 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
package/src/config/file-lock.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import { isEnoent } from "@gajae-code/utils/fs-error";
|
|
3
4
|
|
|
@@ -22,9 +23,10 @@ function getLockPath(filePath: string): string {
|
|
|
22
23
|
return `${filePath}.lock`;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
async function writeLockInfo(lockPath: string): Promise<
|
|
26
|
+
async function writeLockInfo(lockPath: string): Promise<LockInfo> {
|
|
26
27
|
const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
|
|
27
28
|
await Bun.write(`${lockPath}/info`, JSON.stringify(info));
|
|
29
|
+
return info;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
|
|
@@ -51,17 +53,30 @@ export interface FileLockOwnerToken {
|
|
|
51
53
|
timestamp: number;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
/** Outcome of a guarded
|
|
56
|
+
/** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
|
|
55
57
|
export type FileLockGcRemoval = "removed" | "owner_changed" | "missing";
|
|
56
58
|
|
|
59
|
+
interface LockDirStatToken {
|
|
60
|
+
dev: number;
|
|
61
|
+
ino: number;
|
|
62
|
+
mtimeMs: number;
|
|
63
|
+
ctimeMs: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type LockStaleSnapshot =
|
|
67
|
+
| { stale: false }
|
|
68
|
+
| { stale: true; owner: FileLockOwnerToken }
|
|
69
|
+
| { stale: true; owner: null; stat: LockDirStatToken };
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* @internal
|
|
59
|
-
* Fail-closed removal of a
|
|
60
|
-
* token as close to the unlink as possible
|
|
61
|
-
* STILL holds the exact `{pid, timestamp}`
|
|
73
|
+
* Fail-closed removal of a lock dir whose owner is expected to be dead or
|
|
74
|
+
* finished. Re-reads the on-disk owner token as close to the unlink as possible
|
|
75
|
+
* and only deletes the dir when it STILL holds the exact `{pid, timestamp}`
|
|
76
|
+
* identity the caller observed.
|
|
62
77
|
*
|
|
63
|
-
* Closes
|
|
64
|
-
*
|
|
78
|
+
* Closes stale-cleanup TOCTOU windows (#606): between a dead/stale re-read and
|
|
79
|
+
* the unlink, a live process can reclaim a stale lock at the same path
|
|
65
80
|
* (`acquireLock` rms the stale dir, then re-`mkdir`s and rewrites `info` with a
|
|
66
81
|
* fresh pid+timestamp). Deleting by path alone would reap that LIVE lock. Any
|
|
67
82
|
* mismatch (`owner_changed`) or absent/unreadable info (`missing` — e.g. a
|
|
@@ -99,14 +114,28 @@ function ownerLiveness(pid: number): OwnerLiveness {
|
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
function statToken(stats: Stats): LockDirStatToken {
|
|
118
|
+
return {
|
|
119
|
+
dev: stats.dev,
|
|
120
|
+
ino: stats.ino,
|
|
121
|
+
mtimeMs: stats.mtimeMs,
|
|
122
|
+
ctimeMs: stats.ctimeMs,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
|
|
127
|
+
return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function staleLockSnapshot(lockPath: string, staleMs: number): Promise<LockStaleSnapshot> {
|
|
103
131
|
const info = await readLockInfo(lockPath);
|
|
104
132
|
if (!info) {
|
|
105
133
|
try {
|
|
106
134
|
const stats = await fs.stat(lockPath);
|
|
107
|
-
|
|
135
|
+
if (Date.now() - stats.mtimeMs <= staleMs) return { stale: false };
|
|
136
|
+
return { stale: true, owner: null, stat: statToken(stats) };
|
|
108
137
|
} catch (err) {
|
|
109
|
-
if (isEnoent(err)) return false;
|
|
138
|
+
if (isEnoent(err)) return { stale: false };
|
|
110
139
|
throw err;
|
|
111
140
|
}
|
|
112
141
|
}
|
|
@@ -115,53 +144,63 @@ async function isLockStale(lockPath: string, staleMs: number): Promise<boolean>
|
|
|
115
144
|
// not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
|
|
116
145
|
// liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
|
|
117
146
|
const liveness = ownerLiveness(info.pid);
|
|
118
|
-
if (liveness === "
|
|
119
|
-
if (liveness === "
|
|
120
|
-
|
|
147
|
+
if (liveness === "alive") return { stale: false };
|
|
148
|
+
if (liveness === "dead" || Date.now() - info.timestamp > staleMs) {
|
|
149
|
+
return { stale: true, owner: { pid: info.pid, timestamp: info.timestamp } };
|
|
150
|
+
}
|
|
151
|
+
return { stale: false };
|
|
121
152
|
}
|
|
122
153
|
|
|
123
|
-
async function
|
|
154
|
+
async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSnapshot): Promise<boolean> {
|
|
155
|
+
if (!snapshot.stale) return false;
|
|
156
|
+
if (snapshot.owner) {
|
|
157
|
+
return (await removeFileLockDirForGc(lockPath, snapshot.owner)) === "removed";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const currentInfo = await readLockInfo(lockPath);
|
|
161
|
+
if (currentInfo) return false;
|
|
124
162
|
try {
|
|
125
|
-
await fs.
|
|
126
|
-
|
|
163
|
+
const currentStats = await fs.stat(lockPath);
|
|
164
|
+
if (!sameStatToken(statToken(currentStats), snapshot.stat)) return false;
|
|
165
|
+
await fs.rm(lockPath, { recursive: true, force: true });
|
|
127
166
|
return true;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (isEnoent(err)) return false;
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
|
|
174
|
+
try {
|
|
175
|
+
await fs.mkdir(lockPath);
|
|
176
|
+
return await writeLockInfo(lockPath);
|
|
128
177
|
} catch (error) {
|
|
129
178
|
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
130
|
-
return
|
|
179
|
+
return null;
|
|
131
180
|
}
|
|
132
181
|
throw error;
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
184
|
|
|
136
|
-
async function releaseLock(lockPath: string): Promise<void> {
|
|
185
|
+
async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise<void> {
|
|
137
186
|
try {
|
|
138
|
-
await
|
|
187
|
+
await removeFileLockDirForGc(lockPath, owner);
|
|
139
188
|
} catch {
|
|
140
189
|
// Ignore errors on release
|
|
141
190
|
}
|
|
142
191
|
}
|
|
143
|
-
|
|
144
|
-
async function lockExists(lockPath: string): Promise<boolean> {
|
|
145
|
-
try {
|
|
146
|
-
await fs.stat(lockPath);
|
|
147
|
-
return true;
|
|
148
|
-
} catch (err) {
|
|
149
|
-
if (isEnoent(err)) return false;
|
|
150
|
-
throw err;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
192
|
async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
|
|
155
193
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
156
194
|
const lockPath = getLockPath(filePath);
|
|
157
195
|
|
|
158
196
|
for (let attempt = 0; attempt < opts.retries; attempt++) {
|
|
159
|
-
|
|
160
|
-
|
|
197
|
+
const owner = await tryAcquireLock(lockPath);
|
|
198
|
+
if (owner) {
|
|
199
|
+
return () => releaseLock(lockPath, owner);
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
const stale = await staleLockSnapshot(lockPath, opts.staleMs);
|
|
203
|
+
if (await removeStaleLockForAcquire(lockPath, stale)) {
|
|
165
204
|
continue;
|
|
166
205
|
}
|
|
167
206
|
|
|
@@ -134,8 +134,8 @@ export const KEYBINDINGS = {
|
|
|
134
134
|
description: "Queue message for next turn",
|
|
135
135
|
},
|
|
136
136
|
"app.message.dequeue": {
|
|
137
|
-
defaultKeys: "alt+up",
|
|
138
|
-
description: "
|
|
137
|
+
defaultKeys: ["alt+up", "alt+down"],
|
|
138
|
+
description: "Select queued message to edit",
|
|
139
139
|
},
|
|
140
140
|
"app.clipboard.pasteImage": {
|
|
141
141
|
defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
|
|
@@ -76,6 +76,34 @@ export interface MaterializeModelProfileAssignmentOptions {
|
|
|
76
76
|
selector: string;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export interface MaterializeModelProfileAssignmentsOptions {
|
|
80
|
+
session: Pick<
|
|
81
|
+
ModelProfileActivationSession,
|
|
82
|
+
"model" | "thinkingLevel" | "setActiveModelProfile" | "getActiveModelProfile"
|
|
83
|
+
>;
|
|
84
|
+
settings: Pick<Settings, "clearOverride" | "get" | "override" | "set">;
|
|
85
|
+
assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isReadonlyAssignmentMap(
|
|
89
|
+
assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
|
|
90
|
+
): assignments is ReadonlyMap<GjcModelAssignmentTargetId, string> {
|
|
91
|
+
return typeof (assignments as { entries?: unknown }).entries === "function";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getMaterializedAssignments(
|
|
95
|
+
assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
|
|
96
|
+
): Array<[GjcModelAssignmentTargetId, string]> {
|
|
97
|
+
if (isReadonlyAssignmentMap(assignments)) return [...assignments.entries()];
|
|
98
|
+
const assignmentRecord: Partial<Record<GjcModelAssignmentTargetId, string>> = assignments;
|
|
99
|
+
const result: Array<[GjcModelAssignmentTargetId, string]> = [];
|
|
100
|
+
for (const role of Object.keys(assignmentRecord) as GjcModelAssignmentTargetId[]) {
|
|
101
|
+
const selector = assignmentRecord[role];
|
|
102
|
+
if (selector !== undefined) result.push([role, selector]);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
|
|
80
108
|
const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
|
|
81
109
|
if (!activeProfile) return false;
|
|
@@ -109,6 +137,43 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
|
|
|
109
137
|
return true;
|
|
110
138
|
}
|
|
111
139
|
|
|
140
|
+
export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
|
|
141
|
+
const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
|
|
142
|
+
if (!activeProfile) return false;
|
|
143
|
+
|
|
144
|
+
const materializedAssignments = getMaterializedAssignments(options.assignments);
|
|
145
|
+
if (materializedAssignments.length === 0) return true;
|
|
146
|
+
|
|
147
|
+
const nextModelRoles = { ...options.settings.get("modelRoles") };
|
|
148
|
+
const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
|
|
149
|
+
const includesDefault = materializedAssignments.some(([role]) => role === "default");
|
|
150
|
+
|
|
151
|
+
if (!includesDefault && !nextModelRoles.default && options.session.model) {
|
|
152
|
+
nextModelRoles.default = formatModelSelectorValue(
|
|
153
|
+
`${options.session.model.provider}/${options.session.model.id}`,
|
|
154
|
+
options.session.thinkingLevel,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const [role, selector] of materializedAssignments) {
|
|
159
|
+
const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
160
|
+
if (target.settingsPath === "modelRoles") {
|
|
161
|
+
nextModelRoles[role] = selector;
|
|
162
|
+
} else {
|
|
163
|
+
nextAgentModelOverrides[role] = selector;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
options.settings.set("modelRoles", nextModelRoles);
|
|
168
|
+
options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
|
|
169
|
+
options.settings.set("modelProfile.default", undefined);
|
|
170
|
+
options.settings.clearOverride("modelProfile.default");
|
|
171
|
+
options.settings.override("modelRoles", nextModelRoles);
|
|
172
|
+
options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
173
|
+
options.session.setActiveModelProfile?.(undefined);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
112
177
|
export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
|
|
113
178
|
return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
114
179
|
}
|
|
@@ -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"
|
|
@@ -966,24 +967,53 @@ function hasTmuxIdentity(session: Record<string, unknown>): boolean {
|
|
|
966
967
|
);
|
|
967
968
|
}
|
|
968
969
|
|
|
970
|
+
function unavailableSessionReason(turn: TurnRecord, reason: string): string {
|
|
971
|
+
if (
|
|
972
|
+
reason === "tmux_session_missing" &&
|
|
973
|
+
turn.delivery.tmux_keys_sent === true &&
|
|
974
|
+
turn.delivery.prompt_acknowledged === true
|
|
975
|
+
) {
|
|
976
|
+
return "tmux_session_missing_after_prompt_acknowledgement";
|
|
977
|
+
}
|
|
978
|
+
return reason;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function unavailableSessionEvidence(turn: TurnRecord, reason: string, timestamp: string): Record<string, unknown>[] {
|
|
982
|
+
if (reason !== "tmux_session_missing_after_prompt_acknowledgement") return turn.evidence;
|
|
983
|
+
return [
|
|
984
|
+
...turn.evidence,
|
|
985
|
+
{
|
|
986
|
+
type: reason,
|
|
987
|
+
message:
|
|
988
|
+
"The tmux session disappeared after GJC runtime acknowledged the prompt, before any terminal final_response or error was recorded. Treat this as an in-flight vanished session and inspect/restart with recovery evidence rather than resubmitting blindly.",
|
|
989
|
+
tmux_keys_sent: true,
|
|
990
|
+
prompt_acknowledged: true,
|
|
991
|
+
prior_status: turn.status,
|
|
992
|
+
created_at: timestamp,
|
|
993
|
+
},
|
|
994
|
+
];
|
|
995
|
+
}
|
|
996
|
+
|
|
969
997
|
async function markTurnFailedForUnavailableSession(
|
|
970
998
|
namespaceDir: string,
|
|
971
999
|
turn: TurnRecord,
|
|
972
1000
|
reason: string,
|
|
973
1001
|
): Promise<TurnRecord> {
|
|
974
1002
|
const timestamp = new Date().toISOString();
|
|
1003
|
+
const durableReason = unavailableSessionReason(turn, reason);
|
|
975
1004
|
const failed: TurnRecord = {
|
|
976
1005
|
...turn,
|
|
977
1006
|
status: "failed",
|
|
978
1007
|
final_response: {
|
|
979
|
-
text: `Coordinator session unavailable: ${
|
|
1008
|
+
text: `Coordinator session unavailable: ${durableReason}`,
|
|
980
1009
|
format: "markdown",
|
|
981
1010
|
source: "coordinator_liveness",
|
|
982
1011
|
artifact_path: null,
|
|
983
1012
|
truncated: false,
|
|
984
1013
|
},
|
|
985
|
-
|
|
986
|
-
|
|
1014
|
+
evidence: unavailableSessionEvidence(turn, durableReason, timestamp),
|
|
1015
|
+
error: { code: "session_unavailable", message: durableReason, recoverable: true },
|
|
1016
|
+
liveness: { checked_at: timestamp, live: false, reason: durableReason },
|
|
987
1017
|
updated_at: timestamp,
|
|
988
1018
|
completed_at: timestamp,
|
|
989
1019
|
};
|
|
@@ -992,7 +1022,7 @@ async function markTurnFailedForUnavailableSession(
|
|
|
992
1022
|
await writeSessionState(namespaceDir, failed.session_id, "stale", {
|
|
993
1023
|
lastTurnId: failed.turn_id,
|
|
994
1024
|
live: false,
|
|
995
|
-
reason,
|
|
1025
|
+
reason: durableReason,
|
|
996
1026
|
});
|
|
997
1027
|
return failed;
|
|
998
1028
|
}
|
|
@@ -1092,6 +1122,18 @@ async function markTurnAcknowledgedFromRuntimeState(
|
|
|
1092
1122
|
};
|
|
1093
1123
|
await writeTurnRecord(namespaceDir, acknowledged);
|
|
1094
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
|
+
});
|
|
1095
1137
|
return acknowledged;
|
|
1096
1138
|
}
|
|
1097
1139
|
|
|
@@ -1504,14 +1546,27 @@ async function waitForCoordinatorEvents(namespaceDir: string, timeoutMs: number)
|
|
|
1504
1546
|
};
|
|
1505
1547
|
const timer = setTimeout(finish, Math.max(timeoutMs, 0));
|
|
1506
1548
|
timer.unref?.();
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
+
}
|
|
1515
1570
|
}
|
|
1516
1571
|
return deferred.promise;
|
|
1517
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}
|