@gajae-code/coding-agent 0.12.1 → 0.12.4

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 (66) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  4. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
  5. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
  6. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
  7. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  8. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  9. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  10. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  11. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  12. package/dist/types/modes/interactive-mode.d.ts +9 -0
  13. package/dist/types/modes/theme/theme.d.ts +3 -1
  14. package/dist/types/modes/types.d.ts +32 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  16. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
  17. package/dist/types/session/agent-session.d.ts +2 -0
  18. package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
  19. package/dist/types/task/index.d.ts +4 -0
  20. package/dist/types/task/output-manager.d.ts +1 -0
  21. package/dist/types/task/receipt.d.ts +2 -0
  22. package/dist/types/tools/index.d.ts +2 -0
  23. package/package.json +8 -8
  24. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  25. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  26. package/scripts/run-acp-conformance.ts +5 -0
  27. package/src/async/job-manager.ts +22 -20
  28. package/src/config/provider-ranking.ts +1 -0
  29. package/src/config/settings-schema.ts +2 -2
  30. package/src/debug/index.ts +42 -17
  31. package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
  32. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
  33. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
  34. package/src/gjc-runtime/launch-worktree.ts +103 -1
  35. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  36. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  37. package/src/gjc-runtime/ralplan-runtime.ts +80 -10
  38. package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
  39. package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
  40. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  41. package/src/gjc-runtime/workflow-manifest.ts +17 -2
  42. package/src/internal-urls/docs-index.generated.ts +2 -2
  43. package/src/modes/DESIGN.md +53 -0
  44. package/src/modes/components/irc-sidebar.ts +30 -1
  45. package/src/modes/components/tool-status-header.ts +0 -1
  46. package/src/modes/controllers/command-controller.ts +38 -34
  47. package/src/modes/controllers/event-controller.ts +27 -16
  48. package/src/modes/controllers/extension-ui-controller.ts +186 -34
  49. package/src/modes/controllers/selector-controller.ts +115 -22
  50. package/src/modes/interactive-mode.ts +189 -62
  51. package/src/modes/jobs-observer.ts +1 -1
  52. package/src/modes/theme/theme.ts +11 -9
  53. package/src/modes/types.ts +88 -0
  54. package/src/sdk/broker/lifecycle.ts +18 -13
  55. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  56. package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
  57. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  58. package/src/sdk/session.ts +1 -0
  59. package/src/session/agent-session.ts +22 -1
  60. package/src/session/internal/managed-session-scope.ts +158 -107
  61. package/src/session/internal/managed-session-storage.ts +445 -57
  62. package/src/session/session-storage.ts +29 -44
  63. package/src/task/index.ts +257 -31
  64. package/src/task/output-manager.ts +24 -14
  65. package/src/task/receipt.ts +11 -1
  66. package/src/tools/index.ts +2 -0
@@ -1,5 +1,8 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
1
3
  import * as path from "node:path";
2
4
  import { inflateSync } from "node:zlib";
5
+ import { isCompiledBinary } from "@gajae-code/utils/env";
3
6
  import {
4
7
  evidenceKindMatches,
5
8
  hasExistingNonEmptyArtifact,
@@ -401,7 +404,6 @@ function isDeterministicConsoleLogReplay(code: string): boolean {
401
404
  }
402
405
  return matched;
403
406
  }
404
-
405
407
  function hasShellRedirectionToken(value: string): boolean {
406
408
  return /^(?:[<>]|\d?[<>]|\d?>&\d|\|\|?|&&|;)$/.test(value) || /(?:^|[^\w])-?>/.test(value);
407
409
  }
@@ -434,9 +436,6 @@ export function isAllowedGitReplayCommand(args: readonly string[]): boolean {
434
436
  }
435
437
 
436
438
  function isBareExecutableName(value: string): boolean {
437
- // The allowlist is keyed on the basename, but the raw command[0] is what gets spawned.
438
- // Reject path-qualified or case-spoofed executables (e.g. ./git, /tmp/npm, scripts/node, GIT)
439
- // so an attacker-controlled binary cannot impersonate a trusted tool.
440
439
  return (
441
440
  value.length > 0 &&
442
441
  !value.includes("/") &&
@@ -452,20 +451,12 @@ function isAllowedCliReplayCommand(command: readonly string[]): boolean {
452
451
  command.some(arg => arg.trim() !== arg || arg.length === 0 || hasShellRedirectionToken(arg))
453
452
  )
454
453
  return false;
455
- if (!isBareExecutableName(command[0]!)) return false;
456
- const executable = basenameCommand(command[0]!);
454
+ if (!isBareExecutableName(command[0]!) || command[0] !== "bun") return false;
457
455
  const args = command.slice(1);
458
- if (executable === "bun" || executable === "node") {
459
- if (args.length === 1 && args[0] === "--version") return true;
460
- return args.length === 2 && args[0] === "-e" && isDeterministicConsoleLogReplay(args[1]!);
461
- }
462
- if (executable === "npm" || executable === "pnpm" || executable === "yarn") {
463
- return (args.length === 1 && args[0] === "--version") || (args.length === 1 && args[0] === "list");
464
- }
465
- if (executable === "git") return isAllowedGitReplayCommand(args);
466
- if (executable === "gjc") return args.length === 1 && ["read", "status"].includes(args[0] ?? "");
467
- return false;
456
+ if (args.length === 1 && args[0] === "--version") return true;
457
+ return args.length === 2 && args[0] === "-e" && isDeterministicConsoleLogReplay(args[1]!);
468
458
  }
459
+
469
460
  function summarizeBlockedCliReplayCommand(command: readonly string[]): string {
470
461
  const executable = command[0] ? basenameCommand(command[0]) : "<missing>";
471
462
  const argCount = Math.max(0, command.length - 1);
@@ -473,27 +464,44 @@ function summarizeBlockedCliReplayCommand(command: readonly string[]): string {
473
464
  }
474
465
 
475
466
  function cliReplayAllowlistDescription(): string {
476
- return [
477
- '`bun --version`, `node --version`, or deterministic `bun/node -e "console.log(...)"`',
478
- "`npm|pnpm|yarn --version` or `npm|pnpm|yarn list`",
479
- "read-only `git status|rev-parse|merge-base|diff|show|log` with safe args",
480
- "`gjc read` or `gjc status`",
481
- ].join("; ");
467
+ return '`bun --version` or deterministic `bun -e "console.log(...)"`; focused bun test execution is blocked and replayExempt still requires an existing screenshot, automation, or PTY structural fallback';
482
468
  }
483
469
 
484
- function resolveCliReplayCommand(command: string[]): string[] {
485
- if (basenameCommand(command[0]!) === "bun") return [process.execPath, ...command.slice(1)];
486
- return command;
470
+ export function resolveCliReplayCommand(command: string[], options?: { compiled?: boolean }): string[] {
471
+ if (options?.compiled ?? isCompiledBinary()) {
472
+ throw new Error(
473
+ "CLI replay execution is unavailable in the compiled GJC runtime because process.execPath is the GJC application, not a Bun CLI executable",
474
+ );
475
+ }
476
+ return [process.execPath, ...command.slice(1)];
487
477
  }
488
478
 
489
- function resolveUnderCwd(cwd: string, replayCwd: unknown, fieldName: string): string {
479
+ async function resolveUnderCwd(cwd: string, replayCwd: unknown, fieldName: string): Promise<string> {
490
480
  const relative = replayCwd === undefined ? "." : nonEmptyString(replayCwd);
491
481
  if (!relative) throw new Error(`qualityGate ${fieldName}.cwd must be a non-empty string when provided`);
492
- const root = path.resolve(cwd);
493
- const resolved = path.resolve(root, relative);
482
+ const lexicalRoot = path.resolve(cwd);
483
+ const lexical = path.resolve(lexicalRoot, relative);
484
+ const relativeToLexicalRoot = path.relative(lexicalRoot, lexical);
485
+ if (
486
+ relativeToLexicalRoot === ".." ||
487
+ relativeToLexicalRoot.startsWith(`..${path.sep}`) ||
488
+ path.isAbsolute(relativeToLexicalRoot)
489
+ ) {
490
+ throw new Error(`qualityGate ${fieldName}.cwd must resolve under the repository cwd`);
491
+ }
492
+ let root: string;
493
+ let resolved: string;
494
+ try {
495
+ [root, resolved] = await Promise.all([fs.realpath(lexicalRoot), fs.realpath(lexical)]);
496
+ } catch {
497
+ throw new Error(`qualityGate ${fieldName}.cwd must reference an existing directory under the repository cwd`);
498
+ }
494
499
  const relativeToRoot = path.relative(root, resolved);
495
500
  if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToRoot)) {
496
- throw new Error(`qualityGate ${fieldName}.cwd must resolve under the repository cwd`);
501
+ throw new Error(`qualityGate ${fieldName}.cwd must resolve under the repository cwd without symlink escape`);
502
+ }
503
+ if (!(await fs.stat(resolved)).isDirectory()) {
504
+ throw new Error(`qualityGate ${fieldName}.cwd must reference a directory`);
497
505
  }
498
506
  return resolved;
499
507
  }
@@ -535,11 +543,40 @@ function normalizeCliReplayOutput(value: string, cwd: string): string {
535
543
  }
536
544
 
537
545
  export async function readCliReplayRecord(cwd: string, row: JsonObject, fieldName: string): Promise<JsonObject | null> {
538
- const inline = qualityGateObject(row.replay) ?? (row.kind === "cli-replay" ? row : null);
539
- if (inline) return inline;
546
+ const nestedReplay = row.replay === undefined ? null : qualityGateObject(row.replay);
547
+ if (row.replay !== undefined && !nestedReplay) {
548
+ throw new Error(`qualityGate ${fieldName}.replay must be an object when provided`);
549
+ }
550
+ const inlineFieldNames = [
551
+ "schemaVersion",
552
+ "replaySafe",
553
+ "command",
554
+ "cwd",
555
+ "env",
556
+ "timeoutMs",
557
+ "expectedExitCode",
558
+ "recordedStdout",
559
+ "recordedStderr",
560
+ "normalization",
561
+ "invariants",
562
+ "replayExempt",
563
+ ];
564
+ const hasTopLevelInlineFields = inlineFieldNames.some(name => row[name] !== undefined);
565
+ const hasPath = row.path !== undefined;
566
+ if (nestedReplay && (hasPath || hasTopLevelInlineFields)) {
567
+ throw new Error(`qualityGate ${fieldName} must not mix nested replay, artifact path, or top-level replay fields`);
568
+ }
569
+ if (hasTopLevelInlineFields && hasPath) {
570
+ throw new Error(`qualityGate ${fieldName} must not mix inline replay fields with an artifact path`);
571
+ }
572
+ if (nestedReplay) return nestedReplay;
573
+ if (row.kind === "cli-replay" && hasTopLevelInlineFields) return row;
540
574
  if (!evidenceKindMatches(normalizedEvidenceKind(row), ["cli-replay", "command-replay"])) return null;
575
+ if (!hasPath) {
576
+ throw new Error(`qualityGate ${fieldName} CLI replay artifact must provide either replay fields or path`);
577
+ }
541
578
  const bytes = await readArtifactBytes(cwd, row, fieldName);
542
- if (!bytes) return null;
579
+ if (!bytes) throw new Error(`qualityGate ${fieldName} CLI replay artifact path must reference a readable file`);
543
580
  try {
544
581
  return requireQualityGateObject(JSON.parse(bytes.toString("utf8")), `${fieldName}.replay`);
545
582
  } catch (error) {
@@ -557,6 +594,7 @@ function parseCliReplayRecord(
557
594
  timeoutMs: number;
558
595
  expectedExitCode: number;
559
596
  recordedStdout: string;
597
+ recordedStderr: string;
560
598
  invariants: JsonObject[];
561
599
  } {
562
600
  if (record.schemaVersion !== 1) throw new Error(`qualityGate ${fieldName}.schemaVersion must be 1`);
@@ -570,7 +608,7 @@ function parseCliReplayRecord(
570
608
  throw new Error(`qualityGate ${fieldName}.replaySafe must be true before CLI replay executes`);
571
609
  if (!isAllowedCliReplayCommand(command)) {
572
610
  throw new Error(
573
- `qualityGate ${fieldName}.command is not in the conservative CLI replay allowlist; command ${summarizeBlockedCliReplayCommand(command)} is blocked. Allowed replay commands: ${cliReplayAllowlistDescription()}. For other commands, provide audited replayExempt metadata with reasonCode, reason, approvedBy, and fallbackArtifactRefs that point to a structurally valid fallback artifact.`,
611
+ `qualityGate ${fieldName}.command is not in the deterministic CLI replay allowlist; command ${summarizeBlockedCliReplayCommand(command)} is blocked. Allowed replay commands: ${cliReplayAllowlistDescription()}. For other commands, provide audited replayExempt metadata with reasonCode, reason, approvedBy, and fallbackArtifactRefs that point to a structurally valid fallback artifact.`,
574
612
  );
575
613
  }
576
614
  if (record.normalization !== undefined && record.normalization !== "default") {
@@ -594,6 +632,7 @@ function parseCliReplayRecord(
594
632
  timeoutMs: clampCliReplayTimeout(record.timeoutMs),
595
633
  expectedExitCode,
596
634
  recordedStdout: record.recordedStdout,
635
+ recordedStderr: typeof record.recordedStderr === "string" ? record.recordedStderr : "",
597
636
  invariants,
598
637
  };
599
638
  }
@@ -665,11 +704,24 @@ async function collectCliReplayOutput(
665
704
 
666
705
  export interface ReplayProcessHandle {
667
706
  readonly exited: Promise<number>;
707
+ readonly pid?: number;
668
708
  kill(signal?: number | NodeJS.Signals): void;
669
709
  }
670
710
 
711
+ function signalReplayProcessTree(handle: ReplayProcessHandle, signal: NodeJS.Signals): void {
712
+ if (process.platform !== "win32" && Number.isInteger(handle.pid) && (handle.pid ?? 0) > 0) {
713
+ try {
714
+ process.kill(-handle.pid!, signal);
715
+ return;
716
+ } catch (error) {
717
+ if ((error as NodeJS.ErrnoException).code === "ESRCH") return;
718
+ }
719
+ }
720
+ handle.kill(signal);
721
+ }
722
+
671
723
  export async function waitForReplayProcessWithTimeout(
672
- process: ReplayProcessHandle,
724
+ handle: ReplayProcessHandle,
673
725
  timeoutMs: number,
674
726
  graceMs = 2000,
675
727
  ): Promise<number> {
@@ -679,21 +731,21 @@ export async function waitForReplayProcessWithTimeout(
679
731
  const timeout = new Promise<typeof timedOut>(resolve => {
680
732
  timeoutTimer = setTimeout(() => resolve(timedOut), timeoutMs);
681
733
  });
682
- const first = await Promise.race([process.exited, timeout]);
734
+ const first = await Promise.race([handle.exited, timeout]);
683
735
  if (first !== timedOut) {
684
736
  if (timeoutTimer) clearTimeout(timeoutTimer);
685
737
  return first;
686
738
  }
687
- process.kill("SIGTERM");
739
+ signalReplayProcessTree(handle, "SIGTERM");
688
740
  const killed = Symbol("killed");
689
741
  const grace = new Promise<typeof killed>(resolve => {
690
742
  graceTimer = setTimeout(() => {
691
- process.kill("SIGKILL");
743
+ signalReplayProcessTree(handle, "SIGKILL");
692
744
  resolve(killed);
693
745
  }, graceMs);
694
746
  });
695
- await Promise.race([process.exited, grace]);
696
- await process.exited.catch(() => undefined);
747
+ await Promise.race([handle.exited, grace]);
748
+ await handle.exited.catch(() => undefined);
697
749
  if (timeoutTimer) clearTimeout(timeoutTimer);
698
750
  if (graceTimer) clearTimeout(graceTimer);
699
751
  throw new Error("timeout");
@@ -752,18 +804,20 @@ export async function validateCliReplay(
752
804
  }
753
805
  void options.live;
754
806
  const replay = parseCliReplayRecord(record, fieldName);
755
- const replayCwd = resolveUnderCwd(cwd, replay.replayCwd, fieldName);
756
- const process = Bun.spawn(resolveCliReplayCommand(replay.command), {
757
- cwd: replayCwd,
758
- env: replay.env,
759
- stdout: "pipe",
760
- stderr: "pipe",
761
- });
807
+ await resolveUnderCwd(cwd, replay.replayCwd, fieldName);
808
+ const executionCwd = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-cli-replay-"));
762
809
  try {
810
+ const subprocess = Bun.spawn(resolveCliReplayCommand(replay.command), {
811
+ cwd: executionCwd,
812
+ env: { ...replay.env, HOME: executionCwd, TMPDIR: executionCwd },
813
+ stdout: "pipe",
814
+ stderr: "pipe",
815
+ detached: process.platform !== "win32",
816
+ });
763
817
  const [stdout, stderr, exitCode] = await Promise.all([
764
- collectCliReplayOutput(process.stdout),
765
- collectCliReplayOutput(process.stderr),
766
- waitForReplayProcessWithTimeout(process, replay.timeoutMs),
818
+ collectCliReplayOutput(subprocess.stdout),
819
+ collectCliReplayOutput(subprocess.stderr),
820
+ waitForReplayProcessWithTimeout(subprocess, replay.timeoutMs),
767
821
  ]);
768
822
  if (stdout.truncated || stderr.truncated)
769
823
  throw new Error(`qualityGate ${fieldName} CLI replay output exceeded 1 MiB buffer cap`);
@@ -774,6 +828,11 @@ export async function validateCliReplay(
774
828
  }
775
829
  const actualStdout = normalizeCliReplayOutput(stdout.text, cwd);
776
830
  const recordedStdout = normalizeCliReplayOutput(replay.recordedStdout, cwd);
831
+ const actualStderr = normalizeCliReplayOutput(stderr.text, cwd);
832
+ const recordedStderr = normalizeCliReplayOutput(replay.recordedStderr, cwd);
833
+ if (actualStderr !== recordedStderr) {
834
+ throw new Error(`qualityGate ${fieldName} CLI replay stderr did not match recordedStderr after normalization`);
835
+ }
777
836
  if (!replay.invariants.length || !validateCliReplayInvariants(replay.invariants, actualStdout, fieldName)) {
778
837
  if (actualStdout !== recordedStdout) {
779
838
  throw new Error(
@@ -787,6 +846,8 @@ export async function validateCliReplay(
787
846
  throw new Error(`qualityGate ${fieldName} CLI replay timed out after ${replay.timeoutMs}ms`);
788
847
  }
789
848
  throw error;
849
+ } finally {
850
+ await fs.rm(executionCwd, { recursive: true, force: true }).catch(() => undefined);
790
851
  }
791
852
  }
792
853