@gajae-code/coding-agent 0.2.5 → 0.3.0
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 +10 -0
- package/dist/types/async/job-manager.d.ts +84 -2
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/deep-interview/render-middleware.d.ts +5 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +2 -29
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/skill-state/active-state.d.ts +2 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/types.d.ts +55 -3
- package/dist/types/tools/subagent.d.ts +11 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +298 -6
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/harness.ts +592 -0
- package/src/commands/team.ts +36 -39
- package/src/config/settings-schema.ts +7 -0
- package/src/config/settings.ts +5 -0
- package/src/deep-interview/render-middleware.ts +366 -0
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +25 -10
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +132 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +733 -21
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +718 -0
- package/src/gjc-runtime/team-runtime.ts +1083 -89
- package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
- package/src/gjc-runtime/workflow-manifest.ts +425 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +137 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +553 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +97 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +24 -41
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/hook-selector.ts +72 -2
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +9 -1
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +33 -3
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +231 -33
- package/src/session/session-manager.ts +13 -1
- package/src/skill-state/active-state.ts +58 -65
- package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +26 -0
- package/src/task/executor.ts +50 -8
- package/src/task/index.ts +120 -8
- package/src/task/render.ts +6 -3
- package/src/task/types.ts +56 -3
- package/src/tools/ask.ts +28 -7
- package/src/tools/subagent.ts +255 -64
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
1
|
import * as path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
type ActiveSessionScope,
|
|
4
|
+
rebuildActiveSnapshot,
|
|
5
|
+
removeActiveEntry,
|
|
6
|
+
writeActiveEntry,
|
|
7
|
+
} from "../gjc-runtime/state-writer";
|
|
3
8
|
import type { WorkflowStateReceipt } from "./workflow-state-contract";
|
|
4
9
|
|
|
5
10
|
export const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
@@ -56,6 +61,8 @@ export interface SkillActiveState {
|
|
|
56
61
|
session_id?: string;
|
|
57
62
|
thread_id?: string;
|
|
58
63
|
turn_id?: string;
|
|
64
|
+
initialized_mode?: CanonicalGjcWorkflowSkill;
|
|
65
|
+
initialized_state_path?: string;
|
|
59
66
|
active_skills?: SkillActiveEntry[];
|
|
60
67
|
[key: string]: unknown;
|
|
61
68
|
}
|
|
@@ -286,14 +293,6 @@ export function getSkillActiveStatePaths(cwd: string, sessionId?: string): Skill
|
|
|
286
293
|
};
|
|
287
294
|
}
|
|
288
295
|
|
|
289
|
-
async function readStateFile(filePath: string): Promise<SkillActiveState | null> {
|
|
290
|
-
try {
|
|
291
|
-
return normalizeSkillActiveState(JSON.parse(await Bun.file(filePath).text()));
|
|
292
|
-
} catch {
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
296
|
/**
|
|
298
297
|
* Raw read for handoff mutations. Returns the *unnormalized* parsed object so
|
|
299
298
|
* inactive entries remain visible to `rawActiveEntries` — `normalizeSkillActiveState`
|
|
@@ -516,15 +515,41 @@ export async function readVisibleSkillActiveState(cwd: string, sessionId?: strin
|
|
|
516
515
|
};
|
|
517
516
|
}
|
|
518
517
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
518
|
+
function activeStateWriterAudit(verb: string) {
|
|
519
|
+
return { category: "state" as const, verb, owner: "gjc-runtime" as const };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function persistActiveEntry(
|
|
523
|
+
cwd: string,
|
|
524
|
+
sessionScope: ActiveSessionScope | undefined,
|
|
525
|
+
entry: SkillActiveEntry,
|
|
526
|
+
): Promise<void> {
|
|
527
|
+
if (entry.active === false) {
|
|
528
|
+
await removeActiveEntry(cwd, sessionScope, entry.skill, {
|
|
529
|
+
cwd,
|
|
530
|
+
audit: activeStateWriterAudit("remove-active-entry"),
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
534
|
+
cwd,
|
|
535
|
+
audit: activeStateWriterAudit("write-active-entry"),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
522
538
|
}
|
|
523
539
|
|
|
524
|
-
function
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
540
|
+
async function writeHandoffEntry(
|
|
541
|
+
cwd: string,
|
|
542
|
+
sessionScope: ActiveSessionScope | undefined,
|
|
543
|
+
entry: SkillActiveEntry,
|
|
544
|
+
): Promise<void> {
|
|
545
|
+
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
546
|
+
cwd,
|
|
547
|
+
audit: activeStateWriterAudit("write-active-entry"),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function rebuildActiveState(cwd: string, sessionScope?: ActiveSessionScope): Promise<void> {
|
|
552
|
+
await rebuildActiveSnapshot(cwd, sessionScope, { cwd, audit: activeStateWriterAudit("rebuild-active-snapshot") });
|
|
528
553
|
}
|
|
529
554
|
|
|
530
555
|
export async function syncSkillActiveState(options: SyncSkillActiveStateOptions): Promise<void> {
|
|
@@ -545,36 +570,13 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
545
570
|
...(hud ? { hud } : {}),
|
|
546
571
|
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
547
572
|
};
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const rootEntries = upsertEntry(listActiveSkills(rootState), entry, options.active);
|
|
551
|
-
const nextRoot: SkillActiveState = {
|
|
552
|
-
...rootState,
|
|
553
|
-
version: 1,
|
|
554
|
-
active: rootEntries.length > 0,
|
|
555
|
-
skill: rootEntries[0]?.skill ?? "",
|
|
556
|
-
phase: rootEntries[0]?.phase ?? "",
|
|
557
|
-
updated_at: nowIso,
|
|
558
|
-
source: options.source,
|
|
559
|
-
active_skills: rootEntries,
|
|
560
|
-
};
|
|
561
|
-
await writeStateFile(rootPath, nextRoot);
|
|
573
|
+
await persistActiveEntry(options.cwd, undefined, entry);
|
|
574
|
+
await rebuildActiveState(options.cwd);
|
|
562
575
|
|
|
563
|
-
if (!
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
...sessionState,
|
|
568
|
-
version: 1,
|
|
569
|
-
active: sessionEntries.length > 0,
|
|
570
|
-
skill: sessionEntries[0]?.skill ?? "",
|
|
571
|
-
phase: sessionEntries[0]?.phase ?? "",
|
|
572
|
-
session_id: options.sessionId,
|
|
573
|
-
updated_at: nowIso,
|
|
574
|
-
source: options.source,
|
|
575
|
-
active_skills: sessionEntries,
|
|
576
|
-
};
|
|
577
|
-
await writeStateFile(sessionPath, nextSession);
|
|
576
|
+
if (!options.sessionId) return;
|
|
577
|
+
const sessionScope = { sessionId: options.sessionId };
|
|
578
|
+
await persistActiveEntry(options.cwd, sessionScope, entry);
|
|
579
|
+
await rebuildActiveState(options.cwd, sessionScope);
|
|
578
580
|
}
|
|
579
581
|
|
|
580
582
|
export interface ApplyHandoffOptions {
|
|
@@ -604,6 +606,7 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
604
606
|
const sessionId = options.callee.sessionId ?? options.caller.sessionId;
|
|
605
607
|
const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, sessionId);
|
|
606
608
|
const readState = (filePath: string) => readRawActiveStateForHandoff(filePath, options.strict === true);
|
|
609
|
+
await Promise.all([readState(rootPath), ...(sessionPath ? [readState(sessionPath)] : [])]);
|
|
607
610
|
|
|
608
611
|
// A skill can hold more than one visible row in this session's scope — e.g.
|
|
609
612
|
// it was seeded without a session id (rendered globally) and is now handed
|
|
@@ -637,33 +640,23 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
637
640
|
: callerEntry;
|
|
638
641
|
return [...kept, mergedCaller, calleeEntry];
|
|
639
642
|
};
|
|
640
|
-
const
|
|
643
|
+
const writeEntries = async (
|
|
644
|
+
sessionScope: ActiveSessionScope | undefined,
|
|
641
645
|
prior: SkillActiveState | null,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
version: 1,
|
|
649
|
-
active: visible.length > 0,
|
|
650
|
-
skill: visible[0]?.skill ?? "",
|
|
651
|
-
phase: visible[0]?.phase ?? "",
|
|
652
|
-
...(scope === "session" ? { session_id: sessionId } : {}),
|
|
653
|
-
updated_at: nowIso,
|
|
654
|
-
source: options.callee.source ?? options.caller.source,
|
|
655
|
-
active_skills: entries,
|
|
656
|
-
};
|
|
646
|
+
): Promise<void> => {
|
|
647
|
+
const nextEntries = applyEntries(rawActiveEntries(prior));
|
|
648
|
+
for (const entry of nextEntries) {
|
|
649
|
+
await writeHandoffEntry(options.cwd, sessionScope, entry);
|
|
650
|
+
}
|
|
651
|
+
await rebuildActiveState(options.cwd, sessionScope);
|
|
657
652
|
};
|
|
658
653
|
|
|
659
654
|
if (sessionPath) {
|
|
660
655
|
const prior = await readState(sessionPath);
|
|
661
|
-
|
|
662
|
-
await writeStateFile(sessionPath, next);
|
|
656
|
+
await writeEntries({ sessionId }, prior);
|
|
663
657
|
}
|
|
664
658
|
const priorRoot = await readState(rootPath);
|
|
665
|
-
|
|
666
|
-
await writeStateFile(rootPath, nextRoot);
|
|
659
|
+
await writeEntries(undefined, priorRoot);
|
|
667
660
|
}
|
|
668
661
|
|
|
669
662
|
function buildSyncEntry(options: SyncSkillActiveStateOptions, nowIso: string): SkillActiveEntry {
|
|
@@ -14,12 +14,16 @@ import {
|
|
|
14
14
|
export const DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE =
|
|
15
15
|
"Deep-interview phase boundary: continue gathering context/questions/risks and emit a handoff/spec before code edits. Mutation tools and patch execution are blocked while deep-interview is active; finalize specs through `gjc deep-interview --write --stage final` or hand off to an execution phase.";
|
|
16
16
|
export const WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE =
|
|
17
|
-
"
|
|
17
|
+
".gjc workflow state and artifacts are runtime-owned. Agent mutation tools cannot edit `.gjc/**`; use the sanctioned `gjc` CLI instead.";
|
|
18
18
|
|
|
19
|
-
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
|
|
19
|
+
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
|
|
20
20
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
21
21
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
22
22
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
23
|
+
const BASH_TOKEN_RE = /'[^']*'|"(?:\\.|[^"\\])*"|\S+/g;
|
|
24
|
+
const BASH_REDIRECT_RE = /^(?:\d*)>>?$/;
|
|
25
|
+
const BASH_HEREDOC_RE = /^(?:\d*)<<-?$/;
|
|
26
|
+
const BASH_MUTATION_COMMANDS = new Set(["rm", "mv", "cp", "touch", "mkdir", "ln", "tee"]);
|
|
23
27
|
|
|
24
28
|
type ToolWithEditMode = AgentTool & {
|
|
25
29
|
mode?: unknown;
|
|
@@ -219,10 +223,75 @@ function extractEditTargets(args: unknown, tool: ToolWithEditMode): ExtractedTar
|
|
|
219
223
|
return targets;
|
|
220
224
|
}
|
|
221
225
|
|
|
226
|
+
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
227
|
+
const record = getRecord(args);
|
|
228
|
+
const command = safeString(record?.command).trim();
|
|
229
|
+
const targets: ExtractedTargets = { paths: [], unknown: false };
|
|
230
|
+
if (!command) {
|
|
231
|
+
targets.unknown = true;
|
|
232
|
+
return targets;
|
|
233
|
+
}
|
|
234
|
+
if (/^gjc(?:\s|$)/.test(command)) return targets;
|
|
235
|
+
|
|
236
|
+
const tokens = command.match(BASH_TOKEN_RE)?.map(unquoteBashToken) ?? [];
|
|
237
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
238
|
+
const token = tokens[index] ?? "";
|
|
239
|
+
if (BASH_REDIRECT_RE.test(token)) {
|
|
240
|
+
addPath(targets, tokens[index + 1]);
|
|
241
|
+
index++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const redirectMatch = token.match(/^(?:\d*)>>?(.+)$/);
|
|
245
|
+
if (redirectMatch?.[1]) {
|
|
246
|
+
addPath(targets, redirectMatch[1]);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (BASH_HEREDOC_RE.test(token)) {
|
|
250
|
+
addPath(targets, tokens[index + 1]);
|
|
251
|
+
index++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const heredocMatch = token.match(/^(?:\d*)<<-?(.+)$/);
|
|
255
|
+
if (heredocMatch?.[1]) {
|
|
256
|
+
addPath(targets, heredocMatch[1]);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (isMutationBashCommand(tokens, index)) {
|
|
260
|
+
for (let targetIndex = index + 1; targetIndex < tokens.length; targetIndex++) {
|
|
261
|
+
const target = tokens[targetIndex] ?? "";
|
|
262
|
+
if (isBashCommandBoundary(target)) break;
|
|
263
|
+
if (target.startsWith("-")) continue;
|
|
264
|
+
addPath(targets, target);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return targets;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function unquoteBashToken(token: string): string {
|
|
272
|
+
if (token.length < 2) return token;
|
|
273
|
+
const quote = token[0];
|
|
274
|
+
if ((quote === "'" || quote === '"') && token.at(-1) === quote) return token.slice(1, -1);
|
|
275
|
+
return token;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isBashCommandBoundary(token: string): boolean {
|
|
279
|
+
return [";", "&&", "||", "|"].includes(token);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isMutationBashCommand(tokens: string[], index: number): boolean {
|
|
283
|
+
const token = path.basename(tokens[index] ?? "");
|
|
284
|
+
if (BASH_MUTATION_COMMANDS.has(token)) return true;
|
|
285
|
+
if (token !== "sed") return false;
|
|
286
|
+
const next = tokens[index + 1] ?? "";
|
|
287
|
+
return next === "-i" || next.startsWith("-i") || next.includes("i");
|
|
288
|
+
}
|
|
289
|
+
|
|
222
290
|
function extractTargets(tool: ToolWithEditMode, args: unknown): ExtractedTargets {
|
|
223
291
|
if (tool.name === "write") return extractWriteTargets(args);
|
|
224
292
|
if (tool.name === "ast_edit") return extractAstEditTargets(args);
|
|
225
293
|
if (tool.name === "edit") return extractEditTargets(args, tool);
|
|
294
|
+
if (tool.name === "bash") return extractBashTargets(args);
|
|
226
295
|
return { paths: [], unknown: true };
|
|
227
296
|
}
|
|
228
297
|
|
|
@@ -289,6 +358,14 @@ function isAllowlistedPath(cwd: string, rawPath: string): boolean {
|
|
|
289
358
|
if (segments?.[0] !== ".gjc") return false;
|
|
290
359
|
return segments[1] === "specs" || segments[1] === "plans";
|
|
291
360
|
}
|
|
361
|
+
function isBlockedGjcPath(cwd: string, rawPath: string): boolean {
|
|
362
|
+
const segments = relativeGjcSegments(cwd, rawPath);
|
|
363
|
+
return segments?.[0] === ".gjc";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function hasBlockedGjcTarget(cwd: string, targets: ExtractedTargets): boolean {
|
|
367
|
+
return targets.paths.some(rawPath => isBlockedGjcPath(cwd, rawPath));
|
|
368
|
+
}
|
|
292
369
|
|
|
293
370
|
function allTargetsAllowlisted(cwd: string, targets: ExtractedTargets): boolean {
|
|
294
371
|
return (
|
|
@@ -315,18 +392,16 @@ export async function getDeepInterviewMutationDecision(
|
|
|
315
392
|
): Promise<DeepInterviewMutationDecision> {
|
|
316
393
|
if (!BLOCKED_TOOL_NAMES.has(input.tool.name)) return { blocked: false, targets: [] };
|
|
317
394
|
const targets = extractTargets(input.tool, input.args);
|
|
318
|
-
if (input.enforceWorkflowState !== false) {
|
|
395
|
+
if (input.enforceWorkflowState !== false && hasBlockedGjcTarget(input.cwd, targets)) {
|
|
319
396
|
const stateSkill = firstBlockedWorkflowStateSkill(input.cwd, targets);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
};
|
|
329
|
-
}
|
|
397
|
+
const command = stateSkill ? sanctionedWorkflowStateCommand(stateSkill) : "gjc <workflow-command>";
|
|
398
|
+
return {
|
|
399
|
+
blocked: true,
|
|
400
|
+
message: `${WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE}\nUse: ${command}`,
|
|
401
|
+
targets: targets.paths,
|
|
402
|
+
reason: stateSkill ? "workflow-state-target" : "gjc-target",
|
|
403
|
+
command,
|
|
404
|
+
};
|
|
330
405
|
}
|
|
331
406
|
if (!(await isActiveDeepInterview(input.cwd, input.sessionId, input.threadId))) {
|
|
332
407
|
return { blocked: false, targets: [] };
|
|
@@ -340,6 +415,9 @@ export async function getDeepInterviewMutationDecision(
|
|
|
340
415
|
reason: "unknown-target",
|
|
341
416
|
};
|
|
342
417
|
}
|
|
418
|
+
if (input.tool.name === "bash") {
|
|
419
|
+
return { blocked: false, targets: targets.paths };
|
|
420
|
+
}
|
|
343
421
|
return {
|
|
344
422
|
blocked: true,
|
|
345
423
|
message: DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE,
|
|
@@ -13,5 +13,7 @@ import type { CanonicalGjcWorkflowSkill } from "./active-state";
|
|
|
13
13
|
export function initialPhaseForSkill(skill: CanonicalGjcWorkflowSkill | string): string {
|
|
14
14
|
if (skill === "deep-interview") return "interviewing";
|
|
15
15
|
if (skill === "ultragoal") return "goal-planning";
|
|
16
|
+
if (skill === "ralplan") return "planner";
|
|
17
|
+
if (skill === "team") return "starting";
|
|
16
18
|
return "planning";
|
|
17
19
|
}
|
|
@@ -9,6 +9,13 @@ export const WORKFLOW_STATE_RECEIPT_FRESH_MS = 30 * 60 * 1000;
|
|
|
9
9
|
export type WorkflowStateMutationOwner = "gjc-state-cli" | "gjc-runtime" | "gjc-hook";
|
|
10
10
|
export type WorkflowStateReceiptStatus = "fresh" | "stale";
|
|
11
11
|
|
|
12
|
+
export interface WorkflowStateContentChecksum {
|
|
13
|
+
algorithm: "sha256";
|
|
14
|
+
value: string;
|
|
15
|
+
covered_path: string;
|
|
16
|
+
computed_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export interface WorkflowStateReceipt {
|
|
13
20
|
version: 1;
|
|
14
21
|
skill: CanonicalGjcWorkflowSkill;
|
|
@@ -20,6 +27,25 @@ export interface WorkflowStateReceipt {
|
|
|
20
27
|
fresh_until: string;
|
|
21
28
|
status: WorkflowStateReceiptStatus;
|
|
22
29
|
mutation_id: string;
|
|
30
|
+
verb?: string;
|
|
31
|
+
from_phase?: string;
|
|
32
|
+
to_phase?: string;
|
|
33
|
+
forced?: boolean;
|
|
34
|
+
paths?: string[];
|
|
35
|
+
content_sha256?: WorkflowStateContentChecksum;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AuditEntry {
|
|
39
|
+
ts: string;
|
|
40
|
+
skill?: string;
|
|
41
|
+
category: string;
|
|
42
|
+
verb: string;
|
|
43
|
+
owner: WorkflowStateMutationOwner;
|
|
44
|
+
mutation_id: string;
|
|
45
|
+
from_phase?: string;
|
|
46
|
+
to_phase?: string;
|
|
47
|
+
forced: boolean;
|
|
48
|
+
paths: string[];
|
|
23
49
|
}
|
|
24
50
|
|
|
25
51
|
function safeString(value: unknown): string {
|
package/src/task/executor.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } f
|
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
|
|
10
10
|
import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-code/ai/utils/schema";
|
|
11
11
|
import { logger, prompt, untilAborted } from "@gajae-code/utils";
|
|
12
|
+
import { AsyncJobManager } from "../async";
|
|
12
13
|
import { ModelRegistry } from "../config/model-registry";
|
|
13
14
|
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
14
15
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
@@ -112,6 +113,9 @@ export interface ExecutorOptions {
|
|
|
112
113
|
index: number;
|
|
113
114
|
id: string;
|
|
114
115
|
modelOverride?: string | string[];
|
|
116
|
+
runMode?: "initial" | "resume" | "message";
|
|
117
|
+
resumeMessage?: string;
|
|
118
|
+
subagentId?: string;
|
|
115
119
|
/**
|
|
116
120
|
* Active model selector of the parent session, used as an auth-aware fallback
|
|
117
121
|
* if the resolved subagent model has no working credentials. See #985.
|
|
@@ -550,8 +554,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
550
554
|
}
|
|
551
555
|
|
|
552
556
|
// Set up artifact paths and write input file upfront if artifacts dir provided
|
|
553
|
-
let subtaskSessionFile: string | undefined;
|
|
554
|
-
if (options.artifactsDir) {
|
|
557
|
+
let subtaskSessionFile: string | undefined = options.sessionFile ?? undefined;
|
|
558
|
+
if (!subtaskSessionFile && options.artifactsDir) {
|
|
555
559
|
subtaskSessionFile = path.join(options.artifactsDir, `${id}.jsonl`);
|
|
556
560
|
}
|
|
557
561
|
|
|
@@ -617,6 +621,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
617
621
|
let activeSession: AgentSession | null = null;
|
|
618
622
|
let unsubscribe: (() => void) | null = null;
|
|
619
623
|
let yieldCalled = false;
|
|
624
|
+
let pauseRequested = false;
|
|
625
|
+
let paused = false;
|
|
620
626
|
|
|
621
627
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
622
628
|
const accumulatedUsage = {
|
|
@@ -1006,6 +1012,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1006
1012
|
}
|
|
1007
1013
|
}
|
|
1008
1014
|
}
|
|
1015
|
+
paused = (event as { stopReason?: string }).stopReason === "paused";
|
|
1009
1016
|
flushProgress = true;
|
|
1010
1017
|
break;
|
|
1011
1018
|
}
|
|
@@ -1193,10 +1200,27 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1193
1200
|
localProtocolOptions: options.localProtocolOptions,
|
|
1194
1201
|
telemetry: subagentTelemetry,
|
|
1195
1202
|
forkContextSeed: options.forkContextSeed,
|
|
1203
|
+
shouldPause: () => pauseRequested,
|
|
1196
1204
|
}),
|
|
1197
1205
|
);
|
|
1198
1206
|
|
|
1199
1207
|
activeSession = session;
|
|
1208
|
+
const liveSubagentId = options.subagentId ?? id;
|
|
1209
|
+
const manager = AsyncJobManager.instance();
|
|
1210
|
+
if (manager) {
|
|
1211
|
+
manager.registerLiveHandle(liveSubagentId, {
|
|
1212
|
+
requestPause: () => {
|
|
1213
|
+
pauseRequested = true;
|
|
1214
|
+
},
|
|
1215
|
+
injectMessage: async (content, deliverAs) => {
|
|
1216
|
+
if (deliverAs === "nextTurn") {
|
|
1217
|
+
await session.prompt(content, { attribution: "agent" });
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
await session.sendUserMessage(content, { deliverAs });
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1200
1224
|
|
|
1201
1225
|
// Emit lifecycle start event
|
|
1202
1226
|
if (options.eventBus) {
|
|
@@ -1306,6 +1330,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1306
1330
|
progress.retryState = {
|
|
1307
1331
|
attempt: event.attempt,
|
|
1308
1332
|
maxAttempts: event.maxAttempts,
|
|
1333
|
+
unbounded: event.unbounded,
|
|
1309
1334
|
delayMs: event.delayMs,
|
|
1310
1335
|
errorMessage: event.errorMessage,
|
|
1311
1336
|
startedAtMs: Date.now(),
|
|
@@ -1354,13 +1379,24 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1354
1379
|
);
|
|
1355
1380
|
}
|
|
1356
1381
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1382
|
+
const runMode = options.runMode ?? "initial";
|
|
1383
|
+
if (runMode === "message") {
|
|
1384
|
+
await awaitAbortable(session.prompt(options.resumeMessage ?? "", { attribution: "agent" }));
|
|
1385
|
+
await awaitAbortable(session.waitForIdle());
|
|
1386
|
+
} else if (runMode === "resume") {
|
|
1387
|
+
await awaitAbortable(
|
|
1388
|
+
session.prompt("Continue from the paused subagent session state.", { attribution: "agent" }),
|
|
1389
|
+
);
|
|
1390
|
+
await awaitAbortable(session.waitForIdle());
|
|
1391
|
+
} else {
|
|
1392
|
+
await awaitAbortable(session.prompt(task, { attribution: "agent" }));
|
|
1393
|
+
await awaitAbortable(session.waitForIdle());
|
|
1394
|
+
}
|
|
1359
1395
|
|
|
1360
1396
|
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1361
1397
|
|
|
1362
1398
|
let retryCount = 0;
|
|
1363
|
-
while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1399
|
+
while (!paused && !yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1364
1400
|
// Skip reminders when the model returned a terminal error (e.g.
|
|
1365
1401
|
// rate-limit cap hit, auth failure). Re-prompting would just
|
|
1366
1402
|
// hit the same wall, multiplying the failure noise without
|
|
@@ -1390,7 +1426,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1390
1426
|
}
|
|
1391
1427
|
|
|
1392
1428
|
await awaitAbortable(session.waitForIdle());
|
|
1393
|
-
if (!yieldCalled && !abortSignal.aborted) {
|
|
1429
|
+
if (!paused && !yieldCalled && !abortSignal.aborted) {
|
|
1394
1430
|
exitCode = 0;
|
|
1395
1431
|
}
|
|
1396
1432
|
|
|
@@ -1406,6 +1442,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1406
1442
|
exitCode = 1;
|
|
1407
1443
|
error ??= lastAssistant.errorMessage || "Subagent failed";
|
|
1408
1444
|
}
|
|
1445
|
+
if (paused) {
|
|
1446
|
+
exitCode = 0;
|
|
1447
|
+
error = undefined;
|
|
1448
|
+
}
|
|
1409
1449
|
}
|
|
1410
1450
|
} catch (err) {
|
|
1411
1451
|
exitCode = 1;
|
|
@@ -1421,6 +1461,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1421
1461
|
if (exitCode === 0) exitCode = 1;
|
|
1422
1462
|
}
|
|
1423
1463
|
sessionAbortController.abort();
|
|
1464
|
+
AsyncJobManager.instance()?.removeLiveHandle(options.subagentId ?? id);
|
|
1424
1465
|
if (unsubscribe) {
|
|
1425
1466
|
try {
|
|
1426
1467
|
unsubscribe();
|
|
@@ -1527,7 +1568,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1527
1568
|
? yieldAbortReason
|
|
1528
1569
|
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : resolveAbortReasonText()))
|
|
1529
1570
|
: undefined;
|
|
1530
|
-
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1571
|
+
progress.status = paused ? "paused" : wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1531
1572
|
scheduleProgress(true);
|
|
1532
1573
|
|
|
1533
1574
|
// Emit lifecycle end event after finalization so yield status is reflected
|
|
@@ -1537,7 +1578,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1537
1578
|
agent: agent.name,
|
|
1538
1579
|
agentSource: agent.source,
|
|
1539
1580
|
description: options.description,
|
|
1540
|
-
status: progress.status as "completed" | "failed" | "aborted",
|
|
1581
|
+
status: progress.status as "completed" | "failed" | "aborted" | "paused",
|
|
1541
1582
|
sessionFile: subtaskSessionFile,
|
|
1542
1583
|
index,
|
|
1543
1584
|
});
|
|
@@ -1564,6 +1605,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1564
1605
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1565
1606
|
aborted: wasAborted,
|
|
1566
1607
|
abortReason: finalAbortReason,
|
|
1608
|
+
paused,
|
|
1567
1609
|
usage: hasUsage ? accumulatedUsage : undefined,
|
|
1568
1610
|
outputPath,
|
|
1569
1611
|
extractedToolData: progress.extractedToolData,
|