@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.
- package/CHANGELOG.md +39 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
- package/dist/types/task/index.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-acp-conformance.ts +5 -0
- package/src/async/job-manager.ts +22 -20
- package/src/config/provider-ranking.ts +1 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +80 -10
- package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
- package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/DESIGN.md +53 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/tool-status-header.ts +0 -1
- package/src/modes/controllers/command-controller.ts +38 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +186 -34
- package/src/modes/controllers/selector-controller.ts +115 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +1 -0
- package/src/session/agent-session.ts +22 -1
- package/src/session/internal/managed-session-scope.ts +158 -107
- package/src/session/internal/managed-session-storage.ts +445 -57
- package/src/session/session-storage.ts +29 -44
- package/src/task/index.ts +257 -31
- package/src/task/output-manager.ts +24 -14
- package/src/task/receipt.ts +11 -1
- 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 (
|
|
459
|
-
|
|
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 (
|
|
486
|
-
|
|
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
|
|
493
|
-
const
|
|
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
|
|
539
|
-
if (
|
|
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)
|
|
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
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
739
|
+
signalReplayProcessTree(handle, "SIGTERM");
|
|
688
740
|
const killed = Symbol("killed");
|
|
689
741
|
const grace = new Promise<typeof killed>(resolve => {
|
|
690
742
|
graceTimer = setTimeout(() => {
|
|
691
|
-
|
|
743
|
+
signalReplayProcessTree(handle, "SIGKILL");
|
|
692
744
|
resolve(killed);
|
|
693
745
|
}, graceMs);
|
|
694
746
|
});
|
|
695
|
-
await Promise.race([
|
|
696
|
-
await
|
|
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
|
-
|
|
756
|
-
const
|
|
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(
|
|
765
|
-
collectCliReplayOutput(
|
|
766
|
-
waitForReplayProcessWithTimeout(
|
|
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
|
|