@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
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
2
2
|
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
3
3
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
4
|
+
import {
|
|
5
|
+
clampReportedAmbiguity,
|
|
6
|
+
computeAmbiguityFloor,
|
|
7
|
+
disputeFactsFromRetractedRound,
|
|
8
|
+
} from "./deep-interview-ambiguity";
|
|
4
9
|
import {
|
|
5
10
|
answerHash,
|
|
6
11
|
type DeepInterviewEstablishedFact,
|
|
@@ -14,6 +19,7 @@ import {
|
|
|
14
19
|
import { writeSessionActivityMarker } from "./session-resolution";
|
|
15
20
|
import { readExistingStateForMutation, writeGuardedWorkflowEnvelopeAtomic } from "./state-writer";
|
|
16
21
|
|
|
22
|
+
export * from "./deep-interview-ambiguity";
|
|
17
23
|
export * from "./deep-interview-state";
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -395,30 +401,55 @@ export async function syncDeepInterviewRecorderHud(
|
|
|
395
401
|
await syncRecorderHud(cwd, normalizeDeepInterviewEnvelope(read.value), sessionId);
|
|
396
402
|
}
|
|
397
403
|
|
|
398
|
-
/**
|
|
404
|
+
/**
|
|
405
|
+
* Record an `answered` shell for one round (append-or-merge by durable key).
|
|
406
|
+
*
|
|
407
|
+
* Retraction dynamics: replacing an already-`scored` answer is a mechanical
|
|
408
|
+
* contradiction signal (the user pivoted). The facts that round established are
|
|
409
|
+
* marked disputed, which raises the deterministic ambiguity floor immediately —
|
|
410
|
+
* without waiting for the LLM scorer to self-report a trigger.
|
|
411
|
+
*/
|
|
399
412
|
export async function appendOrMergeDeepInterviewRound(
|
|
400
413
|
cwd: string,
|
|
401
414
|
statePath: string,
|
|
402
415
|
input: DeepInterviewAnswerInput,
|
|
403
416
|
options: { sessionId?: string } = {},
|
|
404
|
-
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord }> {
|
|
417
|
+
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord; disputedFactIds?: string[] }> {
|
|
405
418
|
const envelope = await readEnvelope(statePath);
|
|
406
419
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
407
420
|
const shell = buildAnswerShell({ ...input, interviewId });
|
|
408
421
|
const rounds = readRounds(envelope);
|
|
422
|
+
const priorRecord = rounds.find(r => r.round_key === shell.round_key);
|
|
409
423
|
const result = appendOrMergeRound(rounds, shell);
|
|
410
424
|
if (result.action === "noop") {
|
|
411
425
|
await repairRecorderHudFromPersisted(cwd, statePath, options.sessionId);
|
|
412
426
|
return { action: result.action, record: result.record };
|
|
413
427
|
}
|
|
414
|
-
|
|
428
|
+
const inner = envelope.state as Record<string, unknown>;
|
|
429
|
+
inner.rounds = result.rounds;
|
|
430
|
+
let disputedFactIds: string[] | undefined;
|
|
431
|
+
if (result.action === "replaced" && priorRecord?.lifecycle === "scored") {
|
|
432
|
+
const facts = Array.isArray(inner.established_facts) ? inner.established_facts : [];
|
|
433
|
+
const disputed = disputeFactsFromRetractedRound(facts, shell.round);
|
|
434
|
+
if (disputed.disputedIds.length > 0) {
|
|
435
|
+
inner.established_facts = disputed.facts;
|
|
436
|
+
disputedFactIds = disputed.disputedIds;
|
|
437
|
+
// Surface the rise right away: the retraction changed the evidence, so the
|
|
438
|
+
// gating score must reflect the new floor before the next scoring pass.
|
|
439
|
+
const breakdown = computeAmbiguityFloor(inner);
|
|
440
|
+
inner.ambiguity_floor = breakdown;
|
|
441
|
+
if (typeof inner.current_ambiguity === "number") {
|
|
442
|
+
inner.current_ambiguity = clampReportedAmbiguity(inner.current_ambiguity, breakdown.floor).effective;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
415
446
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview record-answer");
|
|
416
447
|
try {
|
|
417
448
|
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
418
449
|
} catch {
|
|
419
450
|
// HUD sync is best-effort cache maintenance and must not change record semantics.
|
|
420
451
|
}
|
|
421
|
-
return { action: result.action, record: result.record };
|
|
452
|
+
return { action: result.action, record: result.record, disputedFactIds };
|
|
422
453
|
}
|
|
423
454
|
|
|
424
455
|
/**
|
|
@@ -459,12 +490,34 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
459
490
|
const envelope = await readEnvelope(statePath);
|
|
460
491
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
461
492
|
const rounds = readRounds(envelope);
|
|
462
|
-
const { rounds:
|
|
493
|
+
const { rounds: enrichedRounds, record: reportedRecord } = enrichRoundWithScoring(rounds, {
|
|
494
|
+
...input,
|
|
495
|
+
interviewId,
|
|
496
|
+
});
|
|
497
|
+
// Deterministic floor (Ouroboros principle): the LLM-reported ambiguity is only a
|
|
498
|
+
// lower-bound input. Evidence persisted in state — unresolved disputed facts,
|
|
499
|
+
// unscored active components, auto-answer dilution — sets a code-computed floor
|
|
500
|
+
// the reported score can never fall under, so a pivot mechanically raises the
|
|
501
|
+
// gating score even when the scorer omits triggers.
|
|
502
|
+
const inner = envelope.state as Record<string, unknown>;
|
|
503
|
+
const breakdown = computeAmbiguityFloor({ ...inner, rounds: enrichedRounds });
|
|
504
|
+
const clampResult = clampReportedAmbiguity(input.ambiguity, breakdown.floor);
|
|
505
|
+
let record = reportedRecord;
|
|
506
|
+
let nextRounds = enrichedRounds;
|
|
507
|
+
if (clampResult.clamped) {
|
|
508
|
+
record = {
|
|
509
|
+
...reportedRecord,
|
|
510
|
+
ambiguity: clampResult.effective,
|
|
511
|
+
reported_ambiguity: input.ambiguity,
|
|
512
|
+
ambiguity_floor: breakdown.floor,
|
|
513
|
+
};
|
|
514
|
+
nextRounds = enrichedRounds.map(item => (item.round_key === record.round_key ? record : item));
|
|
515
|
+
}
|
|
463
516
|
// Fail closed: a scored transition that violates the bidirectional invariant
|
|
464
517
|
// (an active trigger that improves the affected dimension or fails to raise
|
|
465
518
|
// overall ambiguity, or a disputed/unresolved trigger lacking a rationale) must
|
|
466
519
|
// never be persisted — storing it lets the interview falsely converge. Validate
|
|
467
|
-
// against the most recent prior scored round
|
|
520
|
+
// the effective (floor-clamped) record against the most recent prior scored round.
|
|
468
521
|
const prior = latestPriorScoredRound(rounds, record.round_key, record.round);
|
|
469
522
|
const validation = validateDeepInterviewScoredTransition(prior, record);
|
|
470
523
|
if (!validation.ok) {
|
|
@@ -472,8 +525,9 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
472
525
|
`deep-interview scored transition for round ${record.round} is invalid and was refused: ${validation.violations.join("; ")}`,
|
|
473
526
|
);
|
|
474
527
|
}
|
|
475
|
-
|
|
476
|
-
|
|
528
|
+
inner.rounds = nextRounds;
|
|
529
|
+
inner.current_ambiguity = clampResult.effective;
|
|
530
|
+
inner.ambiguity_floor = breakdown;
|
|
477
531
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview score-round");
|
|
478
532
|
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
479
533
|
return { record };
|
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { YAML } from "bun";
|
|
5
6
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
6
7
|
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
7
8
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -19,7 +20,7 @@ export * from "./deep-interview-recorder";
|
|
|
19
20
|
*
|
|
20
21
|
* The CLI itself does not run the Socratic interview; that lives inside the `/skill:deep-interview`
|
|
21
22
|
* skill executed by the agent. This handler validates the documented argument-hint surface
|
|
22
|
-
* (`[--quick|--standard|--deep] <idea>`), seeds `.gjc/state/deep-interview-state.json`, and
|
|
23
|
+
* (`[--trace] [--quick|--standard|--deep] <idea>`), seeds `.gjc/state/deep-interview-state.json`, and
|
|
23
24
|
* updates the shared HUD rail via `syncSkillActiveState` so the active interview is visible to
|
|
24
25
|
* the TUI.
|
|
25
26
|
*/
|
|
@@ -40,6 +41,68 @@ const RESOLUTION_THRESHOLDS = {
|
|
|
40
41
|
deep: 0.35,
|
|
41
42
|
} as const;
|
|
42
43
|
|
|
44
|
+
const TRACE_MAX_RELEVANT_PATHS = 12;
|
|
45
|
+
const TRACE_MAX_PACKAGE_HINTS = 8;
|
|
46
|
+
const TRACE_MAX_DIRECTORY_VISITS = 1200;
|
|
47
|
+
const TRACE_MAX_ENTRY_VISITS = 5000;
|
|
48
|
+
const TRACE_MAX_PENDING_DIRECTORIES = 1200;
|
|
49
|
+
const TRACE_SKIP_DIRS = new Set([
|
|
50
|
+
".git",
|
|
51
|
+
".gjc",
|
|
52
|
+
"node_modules",
|
|
53
|
+
"dist",
|
|
54
|
+
"build",
|
|
55
|
+
"coverage",
|
|
56
|
+
".next",
|
|
57
|
+
".turbo",
|
|
58
|
+
".cache",
|
|
59
|
+
"vendor",
|
|
60
|
+
"target",
|
|
61
|
+
".venv",
|
|
62
|
+
"venv",
|
|
63
|
+
"__pycache__",
|
|
64
|
+
".pytest_cache",
|
|
65
|
+
"tmp",
|
|
66
|
+
"temp",
|
|
67
|
+
"logs",
|
|
68
|
+
"out",
|
|
69
|
+
]);
|
|
70
|
+
const TRACE_SOURCE_EXTENSIONS = new Set([
|
|
71
|
+
".ts",
|
|
72
|
+
".tsx",
|
|
73
|
+
".js",
|
|
74
|
+
".jsx",
|
|
75
|
+
".mts",
|
|
76
|
+
".cts",
|
|
77
|
+
".py",
|
|
78
|
+
".rs",
|
|
79
|
+
".go",
|
|
80
|
+
".java",
|
|
81
|
+
".kt",
|
|
82
|
+
".swift",
|
|
83
|
+
".md",
|
|
84
|
+
".json",
|
|
85
|
+
".yml",
|
|
86
|
+
".yaml",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
interface DeepInterviewTraceSummary {
|
|
90
|
+
enabled: true;
|
|
91
|
+
generated_at: string;
|
|
92
|
+
bounded: true;
|
|
93
|
+
limits: {
|
|
94
|
+
max_relevant_paths: number;
|
|
95
|
+
max_package_hints: number;
|
|
96
|
+
max_directory_visits: number;
|
|
97
|
+
max_entry_visits: number;
|
|
98
|
+
max_pending_directories: number;
|
|
99
|
+
};
|
|
100
|
+
idea_terms: string[];
|
|
101
|
+
project_hints: string[];
|
|
102
|
+
relevant_paths: Array<{ path: string; reason: string }>;
|
|
103
|
+
findings: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
43
106
|
type DeepInterviewResolution = keyof typeof RESOLUTION_THRESHOLDS;
|
|
44
107
|
|
|
45
108
|
class DeepInterviewCommandError extends Error {
|
|
@@ -107,6 +170,134 @@ async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string>
|
|
|
107
170
|
return rawSpec;
|
|
108
171
|
}
|
|
109
172
|
|
|
173
|
+
function traceTerms(idea: string): string[] {
|
|
174
|
+
const terms = new Set<string>();
|
|
175
|
+
for (const match of idea.toLowerCase().matchAll(/[a-z0-9][a-z0-9_-]{2,}/g)) {
|
|
176
|
+
const value = match[0];
|
|
177
|
+
if (["the", "and", "for", "with", "that", "this", "from", "into", "should", "would"].includes(value)) continue;
|
|
178
|
+
terms.add(value);
|
|
179
|
+
if (terms.size >= 12) break;
|
|
180
|
+
}
|
|
181
|
+
return [...terms];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function relativePathReason(relativePath: string, terms: readonly string[]): string | undefined {
|
|
185
|
+
const normalized = relativePath.toLowerCase();
|
|
186
|
+
const matched = terms.find(term => normalized.includes(term));
|
|
187
|
+
if (matched) return `path matches idea term "${matched}"`;
|
|
188
|
+
if (/deep[-_]?interview/i.test(relativePath)) return "path matches deep-interview workflow surface";
|
|
189
|
+
if (/skill|workflow|runtime|state/i.test(relativePath)) return "path matches workflow/runtime surface";
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function readPackageHints(cwd: string): Promise<string[]> {
|
|
194
|
+
const packagePath = path.join(cwd, "package.json");
|
|
195
|
+
let parsed: unknown;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(await fs.readFile(packagePath, "utf-8"));
|
|
198
|
+
} catch {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const manifest = parsed as {
|
|
202
|
+
name?: unknown;
|
|
203
|
+
workspaces?: unknown;
|
|
204
|
+
scripts?: Record<string, unknown>;
|
|
205
|
+
dependencies?: Record<string, unknown>;
|
|
206
|
+
devDependencies?: Record<string, unknown>;
|
|
207
|
+
};
|
|
208
|
+
const hints: string[] = [];
|
|
209
|
+
if (typeof manifest.name === "string") hints.push(`package: ${manifest.name}`);
|
|
210
|
+
if (manifest.workspaces) hints.push("workspace: package.json declares workspaces");
|
|
211
|
+
const scripts = Object.keys(manifest.scripts ?? {}).slice(0, TRACE_MAX_PACKAGE_HINTS);
|
|
212
|
+
if (scripts.length > 0) hints.push(`scripts: ${scripts.join(", ")}`);
|
|
213
|
+
const deps = [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]
|
|
214
|
+
.filter(name => /typescript|bun|react|vite|zod|winston|commander|oclif/i.test(name))
|
|
215
|
+
.slice(0, TRACE_MAX_PACKAGE_HINTS);
|
|
216
|
+
if (deps.length > 0) hints.push(`notable dependencies: ${deps.join(", ")}`);
|
|
217
|
+
return hints.slice(0, TRACE_MAX_PACKAGE_HINTS);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function collectRelevantTracePaths(
|
|
221
|
+
cwd: string,
|
|
222
|
+
terms: readonly string[],
|
|
223
|
+
): Promise<Array<{ path: string; reason: string }>> {
|
|
224
|
+
const results: Array<{ path: string; reason: string; score: number }> = [];
|
|
225
|
+
const pending: Array<{ absolutePath: string; depth: number }> = [{ absolutePath: cwd, depth: 0 }];
|
|
226
|
+
let visitedDirectories = 0;
|
|
227
|
+
let visitedEntries = 0;
|
|
228
|
+
while (
|
|
229
|
+
pending.length > 0 &&
|
|
230
|
+
visitedDirectories < TRACE_MAX_DIRECTORY_VISITS &&
|
|
231
|
+
visitedEntries < TRACE_MAX_ENTRY_VISITS
|
|
232
|
+
) {
|
|
233
|
+
const current = pending.shift();
|
|
234
|
+
if (!current) break;
|
|
235
|
+
visitedDirectories += 1;
|
|
236
|
+
try {
|
|
237
|
+
const directory = await fs.opendir(current.absolutePath);
|
|
238
|
+
for await (const entry of directory) {
|
|
239
|
+
visitedEntries += 1;
|
|
240
|
+
if (visitedEntries > TRACE_MAX_ENTRY_VISITS) break;
|
|
241
|
+
if (TRACE_SKIP_DIRS.has(entry.name)) continue;
|
|
242
|
+
const absolutePath = path.join(current.absolutePath, entry.name);
|
|
243
|
+
const relativePath = path.relative(cwd, absolutePath).split(path.sep).join("/");
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
if (current.depth < 6 && pending.length < TRACE_MAX_PENDING_DIRECTORIES) {
|
|
246
|
+
pending.push({ absolutePath, depth: current.depth + 1 });
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!entry.isFile() || !TRACE_SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue;
|
|
251
|
+
const reason = relativePathReason(relativePath, terms);
|
|
252
|
+
if (!reason) continue;
|
|
253
|
+
const termScore = terms.reduce(
|
|
254
|
+
(score, term) => score + (relativePath.toLowerCase().includes(term) ? 2 : 0),
|
|
255
|
+
0,
|
|
256
|
+
);
|
|
257
|
+
const surfaceScore = /deep[-_]?interview|skill|workflow|runtime|state/i.test(relativePath) ? 1 : 0;
|
|
258
|
+
results.push({ path: relativePath, reason, score: termScore + surfaceScore });
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
}
|
|
262
|
+
return results
|
|
263
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
264
|
+
.slice(0, TRACE_MAX_RELEVANT_PATHS)
|
|
265
|
+
.map(({ path: relativePath, reason }) => ({ path: relativePath, reason }));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function buildDeepInterviewTraceSummary(cwd: string, idea: string): Promise<DeepInterviewTraceSummary> {
|
|
269
|
+
const terms = traceTerms(idea);
|
|
270
|
+
const [projectHints, relevantPaths] = await Promise.all([
|
|
271
|
+
readPackageHints(cwd),
|
|
272
|
+
collectRelevantTracePaths(cwd, terms),
|
|
273
|
+
]);
|
|
274
|
+
const findings = [
|
|
275
|
+
projectHints.length > 0
|
|
276
|
+
? "Project manifest was summarized into bounded package/script/dependency hints."
|
|
277
|
+
: "No readable package.json manifest was found at the project root.",
|
|
278
|
+
relevantPaths.length > 0
|
|
279
|
+
? `Relevant path scan captured ${relevantPaths.length} bounded path hint(s) before interview questions.`
|
|
280
|
+
: "Relevant path scan found no matching source/documentation paths before interview questions.",
|
|
281
|
+
"Trace summary intentionally stores path-level evidence only; raw files and logs are excluded.",
|
|
282
|
+
];
|
|
283
|
+
return {
|
|
284
|
+
enabled: true,
|
|
285
|
+
generated_at: new Date().toISOString(),
|
|
286
|
+
bounded: true,
|
|
287
|
+
limits: {
|
|
288
|
+
max_relevant_paths: TRACE_MAX_RELEVANT_PATHS,
|
|
289
|
+
max_package_hints: TRACE_MAX_PACKAGE_HINTS,
|
|
290
|
+
max_directory_visits: TRACE_MAX_DIRECTORY_VISITS,
|
|
291
|
+
max_entry_visits: TRACE_MAX_ENTRY_VISITS,
|
|
292
|
+
max_pending_directories: TRACE_MAX_PENDING_DIRECTORIES,
|
|
293
|
+
},
|
|
294
|
+
idea_terms: terms,
|
|
295
|
+
project_hints: projectHints,
|
|
296
|
+
relevant_paths: relevantPaths,
|
|
297
|
+
findings,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
110
301
|
interface ResolvedDeepInterviewArgs {
|
|
111
302
|
resolution: DeepInterviewResolution;
|
|
112
303
|
threshold: number;
|
|
@@ -114,6 +305,7 @@ interface ResolvedDeepInterviewArgs {
|
|
|
114
305
|
sessionId: string;
|
|
115
306
|
idea: string;
|
|
116
307
|
language?: DeepInterviewLanguagePreference;
|
|
308
|
+
trace?: DeepInterviewTraceSummary;
|
|
117
309
|
json: boolean;
|
|
118
310
|
}
|
|
119
311
|
|
|
@@ -199,7 +391,7 @@ async function readModernSettingsAmbiguityThreshold(): Promise<{ threshold: numb
|
|
|
199
391
|
const modernConfigPath = modernSettingsPath();
|
|
200
392
|
let parsed: unknown;
|
|
201
393
|
try {
|
|
202
|
-
parsed =
|
|
394
|
+
parsed = YAML.parse(await fs.readFile(modernConfigPath, "utf-8"));
|
|
203
395
|
} catch {
|
|
204
396
|
return undefined;
|
|
205
397
|
}
|
|
@@ -371,6 +563,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
|
|
|
371
563
|
skipNext = true;
|
|
372
564
|
continue;
|
|
373
565
|
}
|
|
566
|
+
if (arg === "--trace") continue;
|
|
374
567
|
if (arg === "--quick" || arg === "--standard" || arg === "--deep" || arg === "--json") continue;
|
|
375
568
|
if (arg.startsWith("-")) {
|
|
376
569
|
throw new DeepInterviewCommandError(2, `unknown flag for gjc deep-interview: ${arg}`);
|
|
@@ -379,6 +572,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
|
|
|
379
572
|
}
|
|
380
573
|
const idea = ideaParts.join(" ").trim();
|
|
381
574
|
const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
|
|
575
|
+
const trace = hasFlag(args, "--trace") && idea ? await buildDeepInterviewTraceSummary(cwd, idea) : undefined;
|
|
382
576
|
return {
|
|
383
577
|
resolution: effectiveResolution,
|
|
384
578
|
threshold,
|
|
@@ -386,6 +580,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
|
|
|
386
580
|
sessionId,
|
|
387
581
|
idea,
|
|
388
582
|
language: resolveDeepInterviewLanguagePreference(idea),
|
|
583
|
+
trace,
|
|
389
584
|
json: hasFlag(args, "--json"),
|
|
390
585
|
};
|
|
391
586
|
}
|
|
@@ -507,6 +702,17 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
|
|
|
507
702
|
},
|
|
508
703
|
updated_at: now,
|
|
509
704
|
};
|
|
705
|
+
if (resolved.trace) {
|
|
706
|
+
payload.trace = resolved.trace;
|
|
707
|
+
(payload.state as Record<string, unknown>).trace = resolved.trace;
|
|
708
|
+
(payload.state as Record<string, unknown>).trace_summary = resolved.trace;
|
|
709
|
+
(payload.state as Record<string, unknown>).codebase_context = {
|
|
710
|
+
source: "trace",
|
|
711
|
+
summary: resolved.trace.findings,
|
|
712
|
+
relevant_paths: resolved.trace.relevant_paths,
|
|
713
|
+
project_hints: resolved.trace.project_hints,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
510
716
|
if (resolved.language) {
|
|
511
717
|
payload.language = resolved.language;
|
|
512
718
|
(payload.state as Record<string, unknown>).language = resolved.language;
|
|
@@ -646,6 +852,7 @@ export async function runNativeDeepInterviewCommand(
|
|
|
646
852
|
threshold_source: resolved.thresholdSource,
|
|
647
853
|
idea: resolved.idea,
|
|
648
854
|
language: resolved.language,
|
|
855
|
+
trace: resolved.trace,
|
|
649
856
|
state_path: statePath,
|
|
650
857
|
handoff: "/skill:deep-interview",
|
|
651
858
|
};
|
|
@@ -654,6 +861,7 @@ export async function runNativeDeepInterviewCommand(
|
|
|
654
861
|
: [
|
|
655
862
|
`deep-interview seed state_path=${statePath}`,
|
|
656
863
|
`resolution=${resolved.resolution} threshold=${resolved.threshold} threshold_source=${resolved.thresholdSource}`,
|
|
864
|
+
resolved.trace ? `trace=enabled bounded_paths=${resolved.trace.relevant_paths.length}` : undefined,
|
|
657
865
|
"handoff=/skill:deep-interview",
|
|
658
866
|
"",
|
|
659
867
|
].join("\n");
|
|
@@ -31,6 +31,13 @@ export interface DeepInterviewEstablishedFact {
|
|
|
31
31
|
dimension?: string;
|
|
32
32
|
evidence?: string;
|
|
33
33
|
disputed: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Resolution pointer for a disputed fact: the id of the fact that replaced it
|
|
36
|
+
* after the user confirmed a pivot. A disputed fact without `superseded_by`
|
|
37
|
+
* keeps the deterministic ambiguity floor elevated; setting it releases the
|
|
38
|
+
* pressure while preserving the contradicted fact for audit.
|
|
39
|
+
*/
|
|
40
|
+
superseded_by?: string;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export interface DeepInterviewTriggerMetadata {
|
|
@@ -66,7 +73,12 @@ export interface DeepInterviewRoundRecord {
|
|
|
66
73
|
answered_at: string;
|
|
67
74
|
scored_at?: string;
|
|
68
75
|
scores?: Record<string, number>;
|
|
76
|
+
/** Effective ambiguity after the deterministic floor clamp (`max(reported, floor)`). */
|
|
69
77
|
ambiguity?: number;
|
|
78
|
+
/** Original LLM-reported ambiguity, preserved for audit when the floor clamped it. */
|
|
79
|
+
reported_ambiguity?: number;
|
|
80
|
+
/** Deterministic floor in effect when this round was scored, when it clamped. */
|
|
81
|
+
ambiguity_floor?: number;
|
|
70
82
|
triggers?: DeepInterviewTriggerMetadata[];
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -125,6 +137,7 @@ const TRANSCRIPT_STATE_FIELDS = [
|
|
|
125
137
|
"rounds",
|
|
126
138
|
"established_facts",
|
|
127
139
|
"current_ambiguity",
|
|
140
|
+
"ambiguity_floor",
|
|
128
141
|
"topology",
|
|
129
142
|
"ontology_snapshots",
|
|
130
143
|
"auto_researched_rounds",
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AssistantMessage } from "@gajae-code/ai";
|
|
5
5
|
import { logger, postmortem } from "@gajae-code/utils";
|
|
6
|
+
import { sessionRuntimeDir } from "./session-layout";
|
|
6
7
|
|
|
7
8
|
export const GJC_COORDINATOR_SESSION_STATE_FILE_ENV = "GJC_COORDINATOR_SESSION_STATE_FILE";
|
|
8
9
|
export const GJC_COORDINATOR_SESSION_ID_ENV = "GJC_COORDINATOR_SESSION_ID";
|
|
@@ -148,6 +149,12 @@ function shouldPreserveTerminalPayload(previous: RuntimeStateSidecarPayload): bo
|
|
|
148
149
|
return source === "agent_end" || source === "launch_error";
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function runtimeStateFileForContext(context: RuntimeStateContext): string | null {
|
|
153
|
+
const explicit = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
154
|
+
if (explicit) return explicit;
|
|
155
|
+
if (!context.sessionId.trim()) return null;
|
|
156
|
+
return path.join(sessionRuntimeDir(context.cwd, context.sessionId), "runtime-state.json");
|
|
157
|
+
}
|
|
151
158
|
function branchForContext(context: RuntimeStateContext): string | null {
|
|
152
159
|
return context.branch ?? (process.env[GJC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
|
|
153
160
|
}
|
|
@@ -160,10 +167,11 @@ function basePayload(input: {
|
|
|
160
167
|
source: string;
|
|
161
168
|
event: string;
|
|
162
169
|
reason: string | null;
|
|
170
|
+
sessionId: string;
|
|
163
171
|
}): Record<string, unknown> {
|
|
164
172
|
return {
|
|
165
173
|
schema_version: 1,
|
|
166
|
-
session_id:
|
|
174
|
+
session_id: input.sessionId,
|
|
167
175
|
state: input.state,
|
|
168
176
|
ready_for_input: input.state === "completed" || input.state === "ready_for_input",
|
|
169
177
|
updated_at: input.now,
|
|
@@ -193,30 +201,51 @@ function numericProcessExitCode(defaultCode: number | null): number | null {
|
|
|
193
201
|
return typeof process.exitCode === "number" ? process.exitCode : defaultCode;
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
function postmortemExitDetails(
|
|
204
|
+
function postmortemExitDetails(
|
|
205
|
+
reason: postmortem.Reason,
|
|
206
|
+
previous: RuntimeStateSidecarPayload,
|
|
207
|
+
): {
|
|
197
208
|
state: RuntimeState;
|
|
198
209
|
reason: string;
|
|
199
210
|
exitKind: string;
|
|
200
211
|
exitCode: number | null;
|
|
201
212
|
signal: string | null;
|
|
202
213
|
error?: { code: string; message: string; recoverable: true };
|
|
214
|
+
recovery?: { action: string; reason: string };
|
|
203
215
|
} {
|
|
204
216
|
if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
|
|
205
217
|
const exitCode = numericProcessExitCode(0) ?? 0;
|
|
206
|
-
const
|
|
218
|
+
const exitedBeforeTerminalState =
|
|
219
|
+
exitCode === 0 && reason === postmortem.Reason.EXIT && previous.state === "running";
|
|
220
|
+
const state: RuntimeState = exitCode === 0 && !exitedBeforeTerminalState ? "completed" : "errored";
|
|
221
|
+
const exitReason = exitedBeforeTerminalState
|
|
222
|
+
? "process_exit_before_terminal_state"
|
|
223
|
+
: reason === postmortem.Reason.EXIT
|
|
224
|
+
? "process_exit"
|
|
225
|
+
: "manual_cleanup";
|
|
207
226
|
return {
|
|
208
227
|
state,
|
|
209
|
-
reason:
|
|
228
|
+
reason: exitReason,
|
|
210
229
|
exitKind: reason,
|
|
211
230
|
exitCode,
|
|
212
231
|
signal: null,
|
|
213
232
|
...(state === "errored"
|
|
214
233
|
? {
|
|
215
234
|
error: {
|
|
216
|
-
code:
|
|
217
|
-
message: publicSafeErrorMessage(
|
|
235
|
+
code: exitReason,
|
|
236
|
+
message: publicSafeErrorMessage(
|
|
237
|
+
exitedBeforeTerminalState
|
|
238
|
+
? "GJC process exited before emitting terminal agent state"
|
|
239
|
+
: `GJC process exited with code ${exitCode}`,
|
|
240
|
+
),
|
|
218
241
|
recoverable: true,
|
|
219
242
|
},
|
|
243
|
+
recovery: {
|
|
244
|
+
action: "recover_or_resume_session",
|
|
245
|
+
reason: exitedBeforeTerminalState
|
|
246
|
+
? "previous runtime state was non-terminal; preserve the worktree and inspect the session before retrying"
|
|
247
|
+
: "process exited with a non-zero status",
|
|
248
|
+
},
|
|
220
249
|
}
|
|
221
250
|
: {}),
|
|
222
251
|
};
|
|
@@ -233,6 +262,7 @@ function postmortemExitDetails(reason: postmortem.Reason): {
|
|
|
233
262
|
exitCode: numericProcessExitCode(null),
|
|
234
263
|
signal: signalByReason[reason] ?? null,
|
|
235
264
|
error: { code: reason, message: errorMessageForPostmortem(reason), recoverable: true },
|
|
265
|
+
recovery: { action: "recover_or_resume_session", reason: "process cleanup ran before terminal agent state" },
|
|
236
266
|
};
|
|
237
267
|
}
|
|
238
268
|
|
|
@@ -250,15 +280,27 @@ export async function persistCoordinatorRuntimeStateFromEvent(
|
|
|
250
280
|
event: RuntimeStateEvent,
|
|
251
281
|
context: RuntimeStateContext,
|
|
252
282
|
): Promise<void> {
|
|
253
|
-
const stateFile =
|
|
283
|
+
const stateFile = runtimeStateFileForContext(context);
|
|
254
284
|
if (!stateFile) return;
|
|
255
285
|
const state = stateForEvent(event);
|
|
256
286
|
if (!state) return;
|
|
257
287
|
const now = new Date().toISOString();
|
|
258
288
|
const previous = readPreviousPayload(stateFile);
|
|
259
289
|
const finalResponse = finalResponseForEvent(event);
|
|
290
|
+
const sessionId = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
|
|
291
|
+
? process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
|
|
292
|
+
: context.sessionId;
|
|
260
293
|
const payload = {
|
|
261
|
-
...basePayload({
|
|
294
|
+
...basePayload({
|
|
295
|
+
context,
|
|
296
|
+
previous,
|
|
297
|
+
state,
|
|
298
|
+
now,
|
|
299
|
+
source: "agent_session_event",
|
|
300
|
+
event: event.type,
|
|
301
|
+
reason: null,
|
|
302
|
+
sessionId,
|
|
303
|
+
}),
|
|
262
304
|
...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
|
|
263
305
|
...(finalResponse ? { final_response: finalResponse } : {}),
|
|
264
306
|
...(state === "errored"
|
|
@@ -282,12 +324,15 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
282
324
|
reason: postmortem.Reason,
|
|
283
325
|
context: RuntimeStateContext,
|
|
284
326
|
): void {
|
|
285
|
-
const stateFile =
|
|
327
|
+
const stateFile = runtimeStateFileForContext(context);
|
|
286
328
|
if (!stateFile) return;
|
|
287
329
|
const previous = readPreviousPayload(stateFile);
|
|
288
330
|
if (shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload)) return;
|
|
289
331
|
const now = new Date().toISOString();
|
|
290
|
-
const details = postmortemExitDetails(reason);
|
|
332
|
+
const details = postmortemExitDetails(reason, previous as RuntimeStateSidecarPayload);
|
|
333
|
+
const sessionId = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
|
|
334
|
+
? process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
|
|
335
|
+
: context.sessionId;
|
|
291
336
|
const payload = {
|
|
292
337
|
...basePayload({
|
|
293
338
|
context,
|
|
@@ -297,6 +342,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
297
342
|
source: "process_postmortem",
|
|
298
343
|
event: "process_exit",
|
|
299
344
|
reason: details.reason,
|
|
345
|
+
sessionId,
|
|
300
346
|
}),
|
|
301
347
|
ended_at: now,
|
|
302
348
|
detected_at: now,
|
|
@@ -304,6 +350,8 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
304
350
|
exit_code: details.exitCode,
|
|
305
351
|
signal: details.signal,
|
|
306
352
|
...(details.error ? { error: details.error } : {}),
|
|
353
|
+
...(details.recovery ? { recovery: details.recovery } : {}),
|
|
354
|
+
previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
|
|
307
355
|
};
|
|
308
356
|
try {
|
|
309
357
|
writeStateFileSync(stateFile, payload);
|
|
@@ -313,7 +361,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
313
361
|
}
|
|
314
362
|
|
|
315
363
|
export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
|
|
316
|
-
if (!
|
|
364
|
+
if (!runtimeStateFileForContext(context)) return () => {};
|
|
317
365
|
return postmortem.register("coordinator-runtime-state", reason => {
|
|
318
366
|
persistCoordinatorRuntimeStateFromPostmortem(reason, context);
|
|
319
367
|
});
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type WorkflowStateReceipt,
|
|
27
27
|
} from "../skill-state/workflow-state-contract";
|
|
28
28
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
29
|
+
import { applyAmbiguityFloorToEnvelope } from "./deep-interview-ambiguity";
|
|
29
30
|
import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
|
|
30
31
|
import { activeSnapshotPath, auditPath, modeStatePath, sessionStateDir } from "./session-layout";
|
|
31
32
|
import {
|
|
@@ -1158,7 +1159,12 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1158
1159
|
|
|
1159
1160
|
const merged =
|
|
1160
1161
|
mode === "deep-interview"
|
|
1161
|
-
?
|
|
1162
|
+
? // Enforce the deterministic ambiguity floor on every reconcile so a
|
|
1163
|
+
// self-reported score can never undercut persisted contradiction evidence.
|
|
1164
|
+
(applyAmbiguityFloorToEnvelope(mergeDeepInterviewEnvelope(existingPayload, payload)).envelope as Record<
|
|
1165
|
+
string,
|
|
1166
|
+
unknown
|
|
1167
|
+
>)
|
|
1162
1168
|
: mergeWithNullDelete(existingPayload, payload);
|
|
1163
1169
|
merged.skill = mode;
|
|
1164
1170
|
merged.current_phase = trimmedPhase;
|
|
@@ -1355,7 +1361,11 @@ async function handleWrite(
|
|
|
1355
1361
|
if (mode === "deep-interview") {
|
|
1356
1362
|
// Deep-interview keeps interview data nested under `state` and merges rounds
|
|
1357
1363
|
// losslessly by durable key; never flatten or delete `state` (that drops recorder history).
|
|
1358
|
-
|
|
1364
|
+
// The deterministic ambiguity floor is applied after the merge so a reported
|
|
1365
|
+
// score written through the CLI can never undercut persisted contradiction evidence.
|
|
1366
|
+
merged = applyAmbiguityFloorToEnvelope(
|
|
1367
|
+
mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") }),
|
|
1368
|
+
).envelope;
|
|
1359
1369
|
} else if (hasFlag(args, "--replace")) {
|
|
1360
1370
|
merged = { ...payload };
|
|
1361
1371
|
} else {
|