@gajae-code/coding-agent 0.7.1 → 0.7.3
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 +57 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +9 -1
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/telegram-daemon.d.ts +73 -16
- package/dist/types/notifications/threaded-inbound.d.ts +19 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -1
- package/dist/types/notifications/topic-registry.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +25 -0
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +10 -8
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/notify-cli.ts +152 -5
- package/src/cli.ts +6 -2
- package/src/commands/mcp.ts +117 -0
- package/src/commands/team.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/settings-schema.ts +30 -1
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +4 -3
- package/src/defaults/gjc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/launch-tmux.ts +17 -3
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-runtime.ts +2 -2
- package/src/gjc-runtime/tmux-common.ts +3 -1
- package/src/gjc-runtime/ultragoal-guard.ts +25 -8
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +7 -2
- package/src/hooks/skill-state.ts +57 -0
- package/src/internal-urls/docs-index.generated.ts +14 -11
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/model-selector.ts +12 -0
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/event-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +3 -0
- package/src/modes/interactive-mode.ts +48 -3
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config.ts +23 -2
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/index.ts +180 -38
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/telegram-daemon.ts +553 -236
- package/src/notifications/threaded-inbound.ts +60 -4
- package/src/notifications/threaded-render.ts +20 -2
- package/src/notifications/topic-registry.ts +5 -0
- package/src/session/agent-session.ts +82 -51
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/fetch.ts +94 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/telegram-send.ts +137 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +159 -0
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
|
@@ -217,6 +217,7 @@ export interface AutoCompactionEndEvent {
|
|
|
217
217
|
errorMessage?: string;
|
|
218
218
|
/** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
|
|
219
219
|
skipped?: boolean;
|
|
220
|
+
continuationSkipReason?: "auto_continue_disabled_non_resumable_tail";
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
/** Fired when auto-retry starts */
|
|
@@ -93,6 +93,10 @@ function hasCurrentGjcVersion(session: GjcTmuxSessionStatus | undefined): boolea
|
|
|
93
93
|
return session?.version === VERSION;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean {
|
|
97
|
+
return Boolean(parsed.continue || parsed.resume || explicitTmuxSessionName(env));
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
function findExistingSessionForLaunch(context: {
|
|
97
101
|
env: NodeJS.ProcessEnv;
|
|
98
102
|
project: string;
|
|
@@ -352,6 +356,11 @@ function readCurrentBranch(cwd: string): string | null {
|
|
|
352
356
|
function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
|
|
353
357
|
spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
|
|
354
358
|
}
|
|
359
|
+
function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
|
|
360
|
+
if (result.signalCode === "SIGHUP") return true;
|
|
361
|
+
const stderr = result.stderr?.toLowerCase() ?? "";
|
|
362
|
+
return stderr.includes("eio") || stderr.includes("input/output error");
|
|
363
|
+
}
|
|
355
364
|
|
|
356
365
|
export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
|
|
357
366
|
const env = context.env ?? process.env;
|
|
@@ -377,14 +386,15 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
377
386
|
tmuxRuntimeSessionPath(cwd, gjcSessionId, buildGjcTmuxSessionSlug(sessionName));
|
|
378
387
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
379
388
|
if (!tmuxAvailable) return undefined;
|
|
380
|
-
const existingSessionName =
|
|
381
|
-
"existingBranchSessionName" in context
|
|
389
|
+
const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
|
|
390
|
+
? "existingBranchSessionName" in context
|
|
382
391
|
? (context.existingBranchSessionName ?? undefined)
|
|
383
392
|
: findExistingSessionForLaunch({
|
|
384
393
|
env,
|
|
385
394
|
project,
|
|
386
395
|
branch,
|
|
387
|
-
})
|
|
396
|
+
})
|
|
397
|
+
: undefined;
|
|
388
398
|
const innerCommand = buildInnerCommand(
|
|
389
399
|
{
|
|
390
400
|
cwd,
|
|
@@ -478,6 +488,10 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
478
488
|
if (created.exitCode !== 0) return false;
|
|
479
489
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
|
|
480
490
|
if (attached.exitCode === 0) return true;
|
|
491
|
+
if (isTmuxAttachDisconnectError(attached)) {
|
|
492
|
+
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
481
495
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
482
496
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
483
497
|
return true;
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
*
|
|
38
38
|
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N> --artifact
|
|
39
39
|
* <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
|
|
40
|
-
* / Critic / revision / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
|
|
40
|
+
* / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
|
|
41
41
|
* an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
|
|
42
42
|
* the HUD chip to reflect the latest persisted stage.
|
|
43
43
|
*/
|
|
@@ -48,7 +48,7 @@ export interface RalplanCommandResult {
|
|
|
48
48
|
stderr?: string;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "adr", "final"] as const;
|
|
51
|
+
const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"] as const;
|
|
52
52
|
type RalplanStage = (typeof KNOWN_STAGES)[number];
|
|
53
53
|
|
|
54
54
|
const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
|
|
@@ -55,7 +55,9 @@ export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
|
|
|
55
55
|
return (
|
|
56
56
|
`the active multiplexer "${tmuxCommand}" lists this session but did not return GJC's ${GJC_TMUX_PROFILE_OPTION} ownership tag; ` +
|
|
57
57
|
"GJC-managed sessions and `gjc team` require a tmux provider that round-trips tmux user options. " +
|
|
58
|
-
"
|
|
58
|
+
"For psmux on Windows, cwd/start-directory flags such as `-c` do not isolate the server namespace; psmux uses the tmux-compatible global `-L <namespace>` flag for that. " +
|
|
59
|
+
"GJC_TMUX_COMMAND and GJC_TEAM_TMUX_COMMAND are binary overrides, not shell command lines, so `psmux -L name` is not a supported value. " +
|
|
60
|
+
"Alternative multiplexers such as psmux on Windows do not reliably persist user options yet, so the Windows-native psmux path is not fully supported; " +
|
|
59
61
|
"use real tmux for GJC-managed session and team flows."
|
|
60
62
|
);
|
|
61
63
|
}
|
|
@@ -65,15 +65,17 @@ function isKnownUltragoalObjective(currentObjective: string): boolean {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
async function ultragoalReadPaths(cwd: string): Promise<UltragoalPaths> {
|
|
68
|
+
async function ultragoalReadPaths(cwd: string): Promise<{ paths: UltragoalPaths; sessionId: string | null }> {
|
|
69
69
|
const envSessionId = process.env.GJC_SESSION_ID?.trim();
|
|
70
|
-
if (envSessionId) return getUltragoalPaths(cwd, envSessionId);
|
|
70
|
+
if (envSessionId) return { paths: getUltragoalPaths(cwd, envSessionId), sessionId: envSessionId };
|
|
71
71
|
try {
|
|
72
72
|
const session = await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
|
|
73
|
-
return getUltragoalPaths(cwd, session.gjcSessionId);
|
|
73
|
+
return { paths: getUltragoalPaths(cwd, session.gjcSessionId), sessionId: session.gjcSessionId };
|
|
74
74
|
} catch (error) {
|
|
75
75
|
if (error instanceof SessionResolutionError && error.code === "no_session") {
|
|
76
|
-
|
|
76
|
+
// No session could be resolved (no env, no auto-detectable active session).
|
|
77
|
+
// Surface the null session id so callers can decide; ask-guard treats it as inactive.
|
|
78
|
+
return { paths: getUltragoalPaths(cwd, null), sessionId: null };
|
|
77
79
|
}
|
|
78
80
|
throw error;
|
|
79
81
|
}
|
|
@@ -82,7 +84,7 @@ async function ultragoalReadPaths(cwd: string): Promise<UltragoalPaths> {
|
|
|
82
84
|
async function hasDurableUltragoalState(cwd: string): Promise<boolean> {
|
|
83
85
|
let paths: UltragoalPaths;
|
|
84
86
|
try {
|
|
85
|
-
paths = await ultragoalReadPaths(cwd);
|
|
87
|
+
({ paths } = await ultragoalReadPaths(cwd));
|
|
86
88
|
} catch (error) {
|
|
87
89
|
if (error instanceof SessionResolutionError) return true;
|
|
88
90
|
throw error;
|
|
@@ -368,14 +370,26 @@ export async function readUltragoalVerificationState(input: {
|
|
|
368
370
|
|
|
369
371
|
export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic> {
|
|
370
372
|
let paths: UltragoalPaths;
|
|
373
|
+
let sessionId: string | null;
|
|
371
374
|
try {
|
|
372
|
-
paths = await ultragoalReadPaths(cwd);
|
|
375
|
+
({ paths, sessionId } = await ultragoalReadPaths(cwd));
|
|
373
376
|
} catch (error) {
|
|
374
377
|
return activeAskDiagnostic({
|
|
375
378
|
reason: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
|
|
376
379
|
source: "durable_state_unreadable",
|
|
377
380
|
});
|
|
378
381
|
}
|
|
382
|
+
// Ultragoal state is session-scoped. When no session can be resolved (no env,
|
|
383
|
+
// no auto-detectable active session) there is no active run to protect, so the
|
|
384
|
+
// ask guard must fall open rather than block on legacy/global durable state.
|
|
385
|
+
if (sessionId === null) {
|
|
386
|
+
return inactiveAskDiagnostic({
|
|
387
|
+
reason: "No active GJC session resolved; ultragoal is inactive.",
|
|
388
|
+
source: "absent",
|
|
389
|
+
goalsPath: paths.goalsPath,
|
|
390
|
+
ledgerPath: paths.ledgerPath,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
379
393
|
try {
|
|
380
394
|
await fs.stat(paths.dir);
|
|
381
395
|
} catch (error) {
|
|
@@ -398,8 +412,8 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
|
|
|
398
412
|
let plan: UltragoalPlan | null;
|
|
399
413
|
let ledger: UltragoalLedgerEvent[];
|
|
400
414
|
try {
|
|
401
|
-
plan = await readUltragoalPlan(cwd);
|
|
402
|
-
ledger = await readUltragoalLedger(cwd);
|
|
415
|
+
plan = await readUltragoalPlan(cwd, sessionId);
|
|
416
|
+
ledger = await readUltragoalLedger(cwd, sessionId);
|
|
403
417
|
} catch (error) {
|
|
404
418
|
return activeAskDiagnostic({
|
|
405
419
|
reason: `Unable to read durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -409,6 +423,9 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
|
|
|
409
423
|
});
|
|
410
424
|
}
|
|
411
425
|
if (!plan) {
|
|
426
|
+
// goals.json absent or empty while the state dir exists is an inconsistent
|
|
427
|
+
// durable state, not a clean "no run". Fail closed so the pause guard (which
|
|
428
|
+
// relies on this `durable_state_unreadable` signal) keeps blocking give-ups.
|
|
412
429
|
return activeAskDiagnostic({
|
|
413
430
|
reason: "Durable .gjc/ultragoal state exists but goals.json is missing or empty.",
|
|
414
431
|
source: "durable_state_unreadable",
|
|
@@ -369,6 +369,9 @@
|
|
|
369
369
|
{
|
|
370
370
|
"id": "revision"
|
|
371
371
|
},
|
|
372
|
+
{
|
|
373
|
+
"id": "post-interview"
|
|
374
|
+
},
|
|
372
375
|
{
|
|
373
376
|
"id": "adr"
|
|
374
377
|
},
|
|
@@ -401,6 +404,26 @@
|
|
|
401
404
|
"to": "revision",
|
|
402
405
|
"verb": "write-artifact"
|
|
403
406
|
},
|
|
407
|
+
{
|
|
408
|
+
"from": "revision",
|
|
409
|
+
"to": "post-interview",
|
|
410
|
+
"verb": "write-artifact"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"from": "critic",
|
|
414
|
+
"to": "post-interview",
|
|
415
|
+
"verb": "write-artifact"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"from": "post-interview",
|
|
419
|
+
"to": "revision",
|
|
420
|
+
"verb": "write-artifact"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"from": "post-interview",
|
|
424
|
+
"to": "adr",
|
|
425
|
+
"verb": "write-artifact"
|
|
426
|
+
},
|
|
404
427
|
{
|
|
405
428
|
"from": "revision",
|
|
406
429
|
"to": "adr",
|
|
@@ -435,6 +458,11 @@
|
|
|
435
458
|
"from": "adr",
|
|
436
459
|
"to": "handoff",
|
|
437
460
|
"verb": "handoff"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"from": "post-interview",
|
|
464
|
+
"to": "handoff",
|
|
465
|
+
"verb": "handoff"
|
|
438
466
|
}
|
|
439
467
|
],
|
|
440
468
|
"typedArgs": [
|
|
@@ -592,6 +620,7 @@
|
|
|
592
620
|
"architect",
|
|
593
621
|
"critic",
|
|
594
622
|
"revision",
|
|
623
|
+
"post-interview",
|
|
595
624
|
"adr",
|
|
596
625
|
"final"
|
|
597
626
|
],
|
|
@@ -172,12 +172,16 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
|
|
|
172
172
|
}),
|
|
173
173
|
ralplan: manifest({
|
|
174
174
|
skill: "ralplan",
|
|
175
|
-
states: ["planner", "architect", "critic", "revision", "adr", "final", "handoff"],
|
|
175
|
+
states: ["planner", "architect", "critic", "revision", "post-interview", "adr", "final", "handoff"],
|
|
176
176
|
terminalStates: ["final", "handoff"],
|
|
177
177
|
transitions: [
|
|
178
178
|
{ from: "planner", to: "architect", verb: "write-artifact" },
|
|
179
179
|
{ from: "architect", to: "critic", verb: "write-artifact" },
|
|
180
180
|
{ from: "critic", to: "revision", verb: "write-artifact" },
|
|
181
|
+
{ from: "revision", to: "post-interview", verb: "write-artifact" },
|
|
182
|
+
{ from: "critic", to: "post-interview", verb: "write-artifact" },
|
|
183
|
+
{ from: "post-interview", to: "revision", verb: "write-artifact" },
|
|
184
|
+
{ from: "post-interview", to: "adr", verb: "write-artifact" },
|
|
181
185
|
{ from: "revision", to: "adr", verb: "write-artifact" },
|
|
182
186
|
{ from: "adr", to: "final", verb: "write-artifact" },
|
|
183
187
|
{ from: "planner", to: "handoff", verb: "handoff" },
|
|
@@ -185,6 +189,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
|
|
|
185
189
|
{ from: "critic", to: "handoff", verb: "handoff" },
|
|
186
190
|
{ from: "revision", to: "handoff", verb: "handoff" },
|
|
187
191
|
{ from: "adr", to: "handoff", verb: "handoff" },
|
|
192
|
+
{ from: "post-interview", to: "handoff", verb: "handoff" },
|
|
188
193
|
],
|
|
189
194
|
verbs: [...stateVerbs(), ...flagVerbs(["kickoff", "write-artifact"]), ...plannedVerbs(PLANNED_ADMIN_VERBS)],
|
|
190
195
|
typedArgs: [
|
|
@@ -196,7 +201,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
|
|
|
196
201
|
{
|
|
197
202
|
name: "stage",
|
|
198
203
|
type: "enum",
|
|
199
|
-
enumValues: ["planner", "architect", "critic", "revision", "adr", "final"],
|
|
204
|
+
enumValues: ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"],
|
|
200
205
|
appliesToVerbs: ["write-artifact"],
|
|
201
206
|
},
|
|
202
207
|
{ name: "stage_n", type: "number", appliesToVerbs: ["write-artifact"] },
|
package/src/hooks/skill-state.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { logger } from "@gajae-code/utils";
|
|
4
4
|
import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
|
|
5
|
+
import { detectDeepInterviewPlaintextAskLeak } from "../deep-interview/plaintext-gate-guard";
|
|
5
6
|
import { activeSnapshotPath, modeStatePath as sessionModeStatePath } from "../gjc-runtime/session-layout";
|
|
6
7
|
import { resolveGjcSessionForRead } from "../gjc-runtime/session-resolution";
|
|
7
8
|
import { ModeStateSchema, SkillActiveStateSchema } from "../gjc-runtime/state-schema";
|
|
@@ -675,6 +676,52 @@ async function readCurrentGoalObjectiveFromSessionFile(sessionFile: string | und
|
|
|
675
676
|
return typeof objective === "string" && objective.trim().length > 0 ? objective.trim() : null;
|
|
676
677
|
}
|
|
677
678
|
|
|
679
|
+
async function readLatestAssistantTextFromSessionFile(sessionFile: string | undefined): Promise<string | null> {
|
|
680
|
+
const trimmed = sessionFile?.trim();
|
|
681
|
+
if (!trimmed) return null;
|
|
682
|
+
let entries: SessionEntry[];
|
|
683
|
+
try {
|
|
684
|
+
entries = (await loadEntriesFromFile(trimmed)).filter((entry): entry is SessionEntry => entry.type !== "session");
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
if (entries.length === 0) return null;
|
|
689
|
+
const context = buildSessionContext(entries);
|
|
690
|
+
for (let index = context.messages.length - 1; index >= 0; index--) {
|
|
691
|
+
const message = context.messages[index];
|
|
692
|
+
if (message?.role !== "assistant") continue;
|
|
693
|
+
const text = message.content
|
|
694
|
+
.filter(block => block.type === "text")
|
|
695
|
+
.map(block => block.text)
|
|
696
|
+
.join("");
|
|
697
|
+
const trimmedText = text.trim();
|
|
698
|
+
return trimmedText.length > 0 ? trimmedText : null;
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function shouldRescueDeepInterviewPlaintextAskLeak(
|
|
704
|
+
skill: GjcWorkflowSkill,
|
|
705
|
+
state: ModeState | null,
|
|
706
|
+
cwd: string,
|
|
707
|
+
sessionFile: string | undefined,
|
|
708
|
+
): Promise<boolean> {
|
|
709
|
+
if (skill !== "deep-interview") return false;
|
|
710
|
+
if (state?.active !== true) return false;
|
|
711
|
+
const phase = String(state.current_phase ?? "")
|
|
712
|
+
.trim()
|
|
713
|
+
.toLowerCase();
|
|
714
|
+
if (DEEP_INTERVIEW_ABORT_PHASES.has(phase)) return false;
|
|
715
|
+
if (await deepInterviewSpecCrystallized(state, cwd)) return false;
|
|
716
|
+
const latestAssistantText = await readLatestAssistantTextFromSessionFile(sessionFile);
|
|
717
|
+
if (!latestAssistantText) return false;
|
|
718
|
+
return detectDeepInterviewPlaintextAskLeak(latestAssistantText) !== null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function buildDeepInterviewPlaintextAskLeakMessage(statePath: string): string {
|
|
722
|
+
return `GJC deep-interview emitted a Deep Interview question/options block as plain text (${statePath}). It must not wait for a prose answer. Continue immediately by calling the ask tool with the Restate gate question and options: Yes, crystallize; Adjust wording; Missing scope; plus free text/custom input.`;
|
|
723
|
+
}
|
|
724
|
+
|
|
678
725
|
export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitStateInput): Promise<string | null> {
|
|
679
726
|
const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
|
|
680
727
|
const visibleModeState = await readVisibleModeState(input.cwd, "ultragoal", resolvedSessionId, input.stateDir);
|
|
@@ -756,6 +803,16 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
|
|
|
756
803
|
systemMessage: recoveryMessage,
|
|
757
804
|
};
|
|
758
805
|
}
|
|
806
|
+
if (await shouldRescueDeepInterviewPlaintextAskLeak(entry.skill, modeState, input.cwd, input.sessionFile)) {
|
|
807
|
+
const statePath = modeStatePath(input.cwd, entry.skill, resolvedSessionId);
|
|
808
|
+
const rescueMessage = buildDeepInterviewPlaintextAskLeakMessage(statePath);
|
|
809
|
+
return {
|
|
810
|
+
decision: "block",
|
|
811
|
+
reason: rescueMessage,
|
|
812
|
+
stopReason: "gjc_skill_deep_interview_plaintext_ask_leak",
|
|
813
|
+
systemMessage: rescueMessage,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
759
816
|
if (modeStateReleasesStop(modeState, handoffRequired)) {
|
|
760
817
|
// A mode-state that claims it releases the Stop block must agree with
|
|
761
818
|
// authoritative durable state. If a stale/incoherent mode-state would
|