@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/async/job-manager.d.ts +84 -2
  3. package/dist/types/commands/harness.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +6 -0
  5. package/dist/types/config/settings.d.ts +2 -0
  6. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  11. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  12. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  17. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  18. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  20. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  21. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  22. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  23. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  24. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  25. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  26. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  27. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  28. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  29. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  30. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  31. package/dist/types/harness-control-plane/types.d.ts +162 -0
  32. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  33. package/dist/types/hooks/skill-state.d.ts +2 -29
  34. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +1 -0
  36. package/dist/types/modes/types.d.ts +1 -0
  37. package/dist/types/sdk.d.ts +2 -0
  38. package/dist/types/session/agent-session.d.ts +8 -0
  39. package/dist/types/skill-state/active-state.d.ts +2 -0
  40. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  41. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  42. package/dist/types/task/executor.d.ts +3 -0
  43. package/dist/types/task/types.d.ts +55 -3
  44. package/dist/types/tools/subagent.d.ts +11 -1
  45. package/package.json +7 -7
  46. package/src/async/job-manager.ts +298 -6
  47. package/src/cli/auth-broker-cli.ts +1 -0
  48. package/src/cli/config-cli.ts +10 -2
  49. package/src/cli.ts +2 -0
  50. package/src/commands/harness.ts +592 -0
  51. package/src/commands/team.ts +36 -39
  52. package/src/config/settings-schema.ts +7 -0
  53. package/src/config/settings.ts +5 -0
  54. package/src/deep-interview/render-middleware.ts +366 -0
  55. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  57. package/src/extensibility/custom-tools/types.ts +1 -0
  58. package/src/extensibility/extensions/types.ts +6 -0
  59. package/src/extensibility/shared-events.ts +1 -0
  60. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  61. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  62. package/src/gjc-runtime/ralplan-runtime.ts +25 -10
  63. package/src/gjc-runtime/state-graph.ts +86 -0
  64. package/src/gjc-runtime/state-migrations.ts +132 -0
  65. package/src/gjc-runtime/state-renderer.ts +345 -0
  66. package/src/gjc-runtime/state-runtime.ts +733 -21
  67. package/src/gjc-runtime/state-validation.ts +49 -0
  68. package/src/gjc-runtime/state-writer.ts +718 -0
  69. package/src/gjc-runtime/team-runtime.ts +1083 -89
  70. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  71. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  72. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  73. package/src/harness-control-plane/classifier.ts +128 -0
  74. package/src/harness-control-plane/control-endpoint.ts +137 -0
  75. package/src/harness-control-plane/finalize.ts +222 -0
  76. package/src/harness-control-plane/frame-mapper.ts +286 -0
  77. package/src/harness-control-plane/operate.ts +225 -0
  78. package/src/harness-control-plane/owner.ts +553 -0
  79. package/src/harness-control-plane/preserve.ts +102 -0
  80. package/src/harness-control-plane/receipts.ts +216 -0
  81. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  82. package/src/harness-control-plane/seams.ts +39 -0
  83. package/src/harness-control-plane/session-lease.ts +388 -0
  84. package/src/harness-control-plane/state-machine.ts +97 -0
  85. package/src/harness-control-plane/storage.ts +257 -0
  86. package/src/harness-control-plane/types.ts +214 -0
  87. package/src/hooks/skill-keywords.ts +4 -2
  88. package/src/hooks/skill-state.ts +24 -41
  89. package/src/internal-urls/docs-index.generated.ts +1 -1
  90. package/src/modes/components/assistant-message.ts +5 -1
  91. package/src/modes/components/hook-selector.ts +72 -2
  92. package/src/modes/controllers/event-controller.ts +71 -6
  93. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  94. package/src/modes/controllers/input-controller.ts +9 -1
  95. package/src/modes/controllers/selector-controller.ts +2 -1
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/types.ts +1 -0
  98. package/src/prompts/agents/executor.md +13 -0
  99. package/src/prompts/tools/subagent.md +33 -3
  100. package/src/sdk.ts +4 -0
  101. package/src/session/agent-session.ts +231 -33
  102. package/src/session/session-manager.ts +13 -1
  103. package/src/skill-state/active-state.ts +58 -65
  104. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  105. package/src/skill-state/initial-phase.ts +2 -0
  106. package/src/skill-state/workflow-state-contract.ts +26 -0
  107. package/src/task/executor.ts +50 -8
  108. package/src/task/index.ts +120 -8
  109. package/src/task/render.ts +6 -3
  110. package/src/task/types.ts +56 -3
  111. package/src/tools/ask.ts +28 -7
  112. 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
- async function writeStateFile(filePath: string, state: SkillActiveState): Promise<void> {
520
- await fs.mkdir(path.dirname(filePath), { recursive: true });
521
- await Bun.write(filePath, `${JSON.stringify(state, null, 2)}\n`);
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 upsertEntry(entries: SkillActiveEntry[], entry: SkillActiveEntry, active: boolean): SkillActiveEntry[] {
525
- const key = entryKey(entry);
526
- const retained = entries.filter(candidate => entryKey(candidate) !== key);
527
- return active ? [...retained, entry] : retained;
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
- const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, options.sessionId);
549
- const rootState = (await readStateFile(rootPath)) ?? { version: 1, active_skills: [] };
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 (!sessionPath) return;
564
- const sessionState = (await readStateFile(sessionPath)) ?? { version: 1, active_skills: [] };
565
- const sessionEntries = upsertEntry(listActiveSkills(sessionState), entry, options.active);
566
- const nextSession: SkillActiveState = {
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 buildNextState = (
643
+ const writeEntries = async (
644
+ sessionScope: ActiveSessionScope | undefined,
641
645
  prior: SkillActiveState | null,
642
- entries: SkillActiveEntry[],
643
- scope: "session" | "root",
644
- ): SkillActiveState => {
645
- const visible = entries.filter(e => e.active !== false);
646
- return {
647
- ...(prior ?? {}),
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
- const next = buildNextState(prior, applyEntries(rawActiveEntries(prior)), "session");
662
- await writeStateFile(sessionPath, next);
656
+ await writeEntries({ sessionId }, prior);
663
657
  }
664
658
  const priorRoot = await readState(rootPath);
665
- const nextRoot = buildNextState(priorRoot, applyEntries(rawActiveEntries(priorRoot)), "root");
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
- "Workflow state JSON is runtime-owned. Use `gjc state <skill> read|write --input '<json>'` for deep-interview, ralplan, ultragoal, and team. Planning artifacts under `.gjc/specs/` and `.gjc/plans/` remain allowed.";
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
- if (stateSkill) {
321
- const command = sanctionedWorkflowStateCommand(stateSkill);
322
- return {
323
- blocked: true,
324
- message: `${WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE}\nUse: ${command}`,
325
- targets: targets.paths,
326
- reason: "workflow-state-target",
327
- command,
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 {
@@ -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
- await awaitAbortable(session.prompt(task, { attribution: "agent" }));
1358
- await awaitAbortable(session.waitForIdle());
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,