@gajae-code/coding-agent 0.12.2 → 0.12.5

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 (40) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/dist/types/async/job-manager.d.ts +9 -1
  3. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
  4. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -2
  5. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -2
  6. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -3
  7. package/dist/types/session/internal/managed-session-storage.d.ts +3 -22
  8. package/dist/types/session/session-manager.d.ts +1 -1
  9. package/dist/types/task/executor.d.ts +1 -1
  10. package/dist/types/task/index.d.ts +2 -1
  11. package/dist/types/task/receipt.d.ts +1 -0
  12. package/dist/types/task/types.d.ts +16 -0
  13. package/dist/types/task/worktree.d.ts +13 -6
  14. package/dist/types/tools/subagent.d.ts +2 -0
  15. package/dist/types/utils/git.d.ts +1 -0
  16. package/package.json +8 -8
  17. package/src/async/job-manager.ts +13 -5
  18. package/src/config/provider-ranking.ts +1 -0
  19. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
  20. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
  21. package/src/gjc-runtime/ultragoal-evidence.ts +160 -31
  22. package/src/gjc-runtime/ultragoal-runtime.ts +231 -19
  23. package/src/internal-urls/docs-index.generated.ts +2 -2
  24. package/src/sdk/broker/lifecycle.ts +13 -18
  25. package/src/sdk/bus/chat-daemon-control.ts +6 -4
  26. package/src/sdk/bus/telegram-daemon-contract.ts +5 -3
  27. package/src/sdk/session.ts +1 -0
  28. package/src/session/internal/managed-session-scope.ts +107 -158
  29. package/src/session/internal/managed-session-storage.ts +57 -445
  30. package/src/session/session-manager.ts +12 -7
  31. package/src/session/session-storage.ts +44 -29
  32. package/src/task/executor.ts +5 -2
  33. package/src/task/index.ts +232 -64
  34. package/src/task/receipt.ts +22 -2
  35. package/src/task/render.ts +12 -1
  36. package/src/task/types.ts +18 -0
  37. package/src/task/worktree.ts +122 -43
  38. package/src/tools/subagent-render.ts +5 -2
  39. package/src/tools/subagent.ts +8 -0
  40. package/src/utils/git.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.5] - 2026-07-30
6
+
7
+ ## [0.12.4] - 2026-07-30
8
+
9
+ ## [0.12.3] - 2026-07-30
10
+
11
+ ### Fixed
12
+
13
+ - The release cut no longer deletes previously released changelog headings whose body is empty. `releasedChangelogContent` ran `removeEmptyVersionEntries` before transforming `## [Unreleased]`, so cutting a release replaced the prior empty semver heading instead of inserting the new one above it — v0.12.2 dropped the `## [0.12.1] - 2026-07-29` heading from four package changelogs. Released sections are now immutable history and are always preserved.
14
+
5
15
  ## [0.12.2] - 2026-07-30
6
16
 
7
17
  ### Added
@@ -15,12 +25,14 @@
15
25
  ### Fixed
16
26
 
17
27
  - Terminal input now normalizes Option/Meta navigation and psmux modified-Enter encodings through the native key parser, keeping legacy, Kitty CSI-u, and modifyOtherKeys behavior consistent.
18
- - Ultragoal CLI replay evidence now accepts any argv `command` instead of a small conservative executable allowlist. Proving a CLI-surface change with the project's own test runner (e.g. `bun test <file>`) was impossible: the allowlist only permitted `--version` probes, `bun/node -e "console.log(...)"` literals, read-only `git`, and `gjc read|status`, so real verification commands were rejected and forced agents into `replayExempt` metadata that additionally demanded a screenshot/automation/PTY fallback artifact. `replaySafe: true` is now the gating attestation, backed by the existing sandbox (subprocess isolation with explicit argv and no shell, timeout kill, 1 MiB output cap, env scrubbing). Every other rail is unchanged and still fails closed: shell-string commands, `replaySafe` not true, dangerous env vars, stdout/`recordedStdout` mismatch, and unexpected exit codes are all still rejected (#3533).
19
- - Ultragoal CLI replay evidence now reads the replay file referenced by an `executorQa.artifactRefs` entry whose `kind` is `cli-replay`. Such a row short-circuited as an *inline* replay record, so its `path` was never read and validation failed with a confusing `schemaVersion must be 1` diagnostic pointing at the ref instead of the file — forcing an undocumented workaround of renaming the ref's `kind`. A row is now treated as inline only when it actually carries replay fields (#3533).
28
+ - Ultragoal CLI replay no longer executes model-authored test source or trusts `replaySafe: true` as arbitrary command authority. Runtime replay is limited to the pinned Bun runtime for `--version` and literal `-e "console.log(...)"`; shells, interpreter code strings, path-qualified executables, tests, install/publish/network/git mutation commands, and arbitrary argv are rejected. Replay cwd/artifact files are realpath-confined, ambiguous rows fail closed, stdout and stderr are checked, and POSIX timeout cleanup signals the process group. Structured test-report fallback remains deliberately unsupported pending a separate trusted-provenance design (#3533).
29
+ - Ultragoal CLI replay evidence now reads the replay file referenced by an `executorQa.artifactRefs` entry whose `kind` is `cli-replay`. Inline, nested, and file-backed replay forms are disambiguated explicitly; mixed or malformed rows fail closed (#3533).
30
+
20
31
  - Broker artifact cleanup no longer promotes a non-empty `cleanup_pending` quarantine to transcript-phase completion. The broker advances only when the retained quarantine is root-only/empty or when the lower layer returns `artifacts_removed`, so artifact bytes cannot vanish behind a success receipt (#3489).
21
32
  - `gjc --worktree` / `gjc -w` launch no longer crashes with a raw uncaught `EEXIST` when the worktree bucket directory (`<repo>.gajae-code-worktrees`) is a broken symbolic link to unmounted or offloaded cold storage. The launch distinguishes dangling links and non-directory entries from valid directory symlinks or Windows junctions, reclassifies mkdir races, avoids disclosing raw link targets or unsafe shell commands, and never deletes or replaces an obstructing entry.
22
33
  - POSIX parent identity reproof/fsync is now centralized before every promotable artifact-phase result, preventing a crash-window where a rename is lost after durable retirement is recorded (#3489).
23
34
  - Artifact retirement, planned paths, retained authority, and transcript retry in both managed reconciliation and deletion now bind to the newest published `pendingEvidence` attempt, preventing stranded detached transcripts at paths absent from the newest receipt after a crash (#3489).
35
+ - Provider retry classification prefers the typed `stream_first_event_timeout` transport fact when present, falling back to error-message regex for message-only callers (#3496).
24
36
  - Detached task receipts for in-memory parent sessions no longer advertise dead `agent://` output URIs. TaskTool allocates a session-lifetime durable artifact root under the process temp directory, persists child outputs there, authorizes parent and same-session descendants for scoped resolution, and omits the URI entirely when durable allocation fails (#3471).
25
37
  - Managed-session replacement and cleanup now bind Windows destination mutation to exact native identity, keep lock acquisition/release retryable without reviving lost ownership, and report retained artifact payloads as `cleanup_pending` until only the verified root remains.
26
38
  - Resuming a session no longer crashes with an unhandled rejection when another session transition is already running. The session picker dispatches resume through a void-returning callback, and `handleResumeSession` had no re-entrancy guard, so a second selection (or a resume issued while compaction, handoff, or a fork was in flight) reached `switchSession` and the `{ code: "busy" }` transition error rejected a promise nobody awaited. Resume now ignores an overlapping request with a status message, reports a busy transition as status, and still propagates every other failure. The progress lease is released on all paths.
@@ -44,6 +56,7 @@
44
56
  ### Added
45
57
 
46
58
  - User-created Telegram forum topics can now start a GJC session by selecting the home folder, choosing a verified recent work folder, or entering an explicit folder path. The selected topic is adopted by the new session without creating or deleting a separate Telegram topic.
59
+ - The interactive terminal’s responsive IRC/todo work-lane contract now covers exact narrow/wide geometry, requested versus effective IRC visibility, direct-root pin ordering, todo lane bounds, remapped IRC toggles, and live composer shortcut hints.
47
60
 
48
61
  ### Fixed
49
62
 
@@ -134,6 +134,8 @@ export interface SubagentRecord {
134
134
  effectiveModel?: string;
135
135
  /** True when the requested model lacked credentials and the subagent fell back to the parent model. */
136
136
  modelFellBack?: boolean;
137
+ /** True when the effective subagent provider is in fast mode. */
138
+ fastMode?: boolean;
137
139
  }
138
140
  /** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
139
141
  export interface ResumeDescriptor {
@@ -251,11 +253,17 @@ export declare class AsyncJobManager {
251
253
  };
252
254
  /** Register or replace the canonical record for a subagent. */
253
255
  registerSubagentRecord(record: SubagentRecord): void;
254
- /** Patch model metadata onto an existing subagent record (best-effort; no-op if unknown). */
256
+ /**
257
+ * Patch model/runtime metadata onto an existing subagent record (best-effort; no-op if
258
+ * unknown). Every field is optional and omitting one preserves its current value, so a
259
+ * narrow patch like `{ fastMode: true }` cannot erase model identity recorded earlier.
260
+ * A field therefore cannot be cleared back to `undefined` through this method.
261
+ */
255
262
  updateSubagentModel(subagentId: string, model: {
256
263
  requestedModel?: string;
257
264
  effectiveModel?: string;
258
265
  modelFellBack?: boolean;
266
+ fastMode?: boolean;
259
267
  }): void;
260
268
  getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined;
261
269
  getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[];
@@ -17,12 +17,16 @@ export declare function validateStructuralArtifact(cwd: string, row: JsonObject,
17
17
  live: boolean;
18
18
  }): Promise<boolean>;
19
19
  export declare function isAllowedGitReplayCommand(args: readonly string[]): boolean;
20
+ export declare function resolveCliReplayCommand(command: string[], options?: {
21
+ compiled?: boolean;
22
+ }): string[];
20
23
  export declare function readCliReplayRecord(cwd: string, row: JsonObject, fieldName: string): Promise<JsonObject | null>;
21
24
  export interface ReplayProcessHandle {
22
25
  readonly exited: Promise<number>;
26
+ readonly pid?: number;
23
27
  kill(signal?: number | NodeJS.Signals): void;
24
28
  }
25
- export declare function waitForReplayProcessWithTimeout(process: ReplayProcessHandle, timeoutMs: number, graceMs?: number): Promise<number>;
29
+ export declare function waitForReplayProcessWithTimeout(handle: ReplayProcessHandle, timeoutMs: number, graceMs?: number): Promise<number>;
26
30
  export declare function validateReplayExemptFallback(cwd: string, record: JsonObject, fieldName: string, artifactRefs: Map<string, JsonObject>, options: {
27
31
  surfaceFamily: SurfaceFamily;
28
32
  live: boolean;
@@ -315,9 +315,9 @@ export declare function isSubstantiveEvidence(value: unknown): boolean;
315
315
  export declare function hasTypedVerifiedReceipt(value: unknown): boolean;
316
316
  export declare function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean>;
317
317
  export declare function readArtifactBytes(cwd: string, row: JsonObject, fieldName: string): Promise<Buffer | null>;
318
- import { validateArtifactProof, validateCliReplay, validateLiveSurfaceProofPresence, validateReplayExemptFallback, validateStructuralArtifact, validateSurfaceStructuralRequirement, waitForReplayProcessWithTimeout } from "./ultragoal-evidence";
318
+ import { resolveCliReplayCommand, validateArtifactProof, validateCliReplay, validateLiveSurfaceProofPresence, validateReplayExemptFallback, validateStructuralArtifact, validateSurfaceStructuralRequirement, waitForReplayProcessWithTimeout } from "./ultragoal-evidence";
319
319
  export type { ReplayProcessHandle } from "./ultragoal-evidence";
320
- export { validateArtifactProof, validateCliReplay, validateLiveSurfaceProofPresence, validateReplayExemptFallback, validateStructuralArtifact, validateSurfaceStructuralRequirement, waitForReplayProcessWithTimeout, };
320
+ export { resolveCliReplayCommand, validateArtifactProof, validateCliReplay, validateLiveSurfaceProofPresence, validateReplayExemptFallback, validateStructuralArtifact, validateSurfaceStructuralRequirement, waitForReplayProcessWithTimeout, };
321
321
  export declare function validateExecutorQaRedTeamEvidenceForReview(cwd: string, executorQa: Record<string, unknown>, options?: {
322
322
  mode?: "review";
323
323
  changeSet?: UltragoalChangeSet;
@@ -19,8 +19,10 @@ export type ChatDaemonAction = "stop" | "reload";
19
19
  * chat daemons after notification configuration parsing changes. Generation 15
20
20
  * applies the current notification configuration directly when starting chat
21
21
  * daemon transports. Generation 16 applies Telegram sound-policy configuration
22
- * through shared notification parsing. Generation 17 binds managed-session
23
- * replacement to exact native filesystem authority.
22
+ * through shared notification parsing. Generation 17 bound managed-session
23
+ * replacement to exact native filesystem authority; generation 18 retires that
24
+ * binding (revert of #3489, which stalled POSIX artifact cleanup) while keeping
25
+ * the lifecycle contract moving strictly forward.
24
26
  */
25
27
  export declare const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>>;
26
28
  export declare function chatDaemonGeneration(kind: ChatDaemonKind): number;
@@ -44,10 +44,12 @@ export declare const NOTIFICATION_PROTOCOL_VERSION = 3;
44
44
  * 33 adds action-bound multi-select state rendering and replay-safe option
45
45
  * snapshots. Generation 34 converts non-photo image formats (including WebP)
46
46
  * into Telegram-compatible photo uploads when possible. Generation 35 adds
47
- * user-created topic adoption (forum-topic folder picker). Generation 36 binds
48
- * managed-session replacement to exact native filesystem authority.
47
+ * user-created topic adoption (forum-topic folder picker). Generation 36 bound
48
+ * managed-session replacement to exact native filesystem authority; generation
49
+ * 37 retires that binding (revert of #3489, which stalled POSIX artifact
50
+ * cleanup) while keeping the lifecycle contract moving strictly forward.
49
51
  */
50
- export declare const DAEMON_GENERATION = 36;
52
+ export declare const DAEMON_GENERATION = 37;
51
53
  /**
52
54
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
53
55
  * all builds published before this field existed; epoch 2 covered generation 29;
@@ -7,7 +7,6 @@ export declare const MANAGED_ARTIFACT_MAX_TOTAL_BYTES: number;
7
7
  export declare const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
8
8
  export declare class ManagedPublishError extends Error {
9
9
  readonly classification: "destination_conflict" | "atomic_unavailable" | "invalid_request" | "durability_not_provable" | "identity_mismatch" | "io_error" | "durability_failed";
10
- readonly code: ManagedPublishError["classification"];
11
10
  readonly diagnostic: string;
12
11
  constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome);
13
12
  }
@@ -21,12 +20,6 @@ export interface ManagedStorageLock {
21
20
  assertOwned(): void;
22
21
  release(): Promise<void>;
23
22
  }
24
- export interface ManagedStorageLockSync {
25
- path: string;
26
- attemptId: string;
27
- assertOwned(): void;
28
- release(): void;
29
- }
30
23
  export interface ManagedFileSnapshot {
31
24
  bytes: Buffer;
32
25
  identity: {
@@ -38,13 +31,6 @@ export interface ManagedFileSnapshot {
38
31
  sha256: string;
39
32
  };
40
33
  }
41
- export declare const MANAGED_SESSION_SECURITY_CODES: readonly ["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown", "acl_unavailable", "acl_apply_failed", "acl_verify_failed", "not_found", "not_directory", "network_unsupported", "reparse_point", "identity_unavailable", "identity_mismatch", "owner_mismatch", "mode_mismatch", "io_error"];
42
- export type ManagedSessionSecurityCode = (typeof MANAGED_SESSION_SECURITY_CODES)[number];
43
- export declare class ManagedSessionSecurityError extends Error {
44
- readonly code: ManagedSessionSecurityCode;
45
- constructor(code: ManagedSessionSecurityCode);
46
- }
47
- export declare function managedSessionSecurityCode(error: unknown): ManagedSessionSecurityCode | undefined;
48
34
  export declare function validateNativeSecurityResult(value: unknown, operation: "apply" | "verify", kind: "directory" | "file"): NativeOwnerOnlySecurityResult;
49
35
  /** Captured configured-root authority for managed paths only. */
50
36
  export interface ManagedDirectoryRoot {
@@ -119,18 +105,13 @@ export declare function captureManagedFileNoFollow(pathname: string): ManagedFil
119
105
  /** Atomically publishes bytes without replacing an existing destination. */
120
106
  export declare function publishManagedFileNoReplace(destination: string, bytes: Uint8Array, assertOwned?: () => void, root?: ManagedDirectoryRoot, policy?: ManagedSessionSecurityPolicy): Promise<void>;
121
107
  export declare function publishManagedFileNoReplaceSync(destination: string, bytes: Uint8Array, root?: ManagedDirectoryRoot, policy?: ManagedSessionSecurityPolicy): void;
122
- /**
123
- * Atomically replace a managed file only when the destination is still the
124
- * exact captured object. Unlike `replaceManagedFileSync`, this never performs
125
- * an unconditional pathname rename.
126
- */
127
- export declare function replaceManagedFileExactSync(destination: string, bytes: Uint8Array, expected: Pick<ManagedFileSnapshot, "identity">, root: ManagedDirectoryRoot): void;
108
+ /** Replace one managed regular file while retaining the secured staging fd through publication. */
109
+ export declare function replaceManagedFileSync(destination: string, bytes: Uint8Array, root: ManagedDirectoryRoot, policy?: ManagedSessionSecurityPolicy): void;
110
+ export declare function replaceManagedFile(destination: string, bytes: Uint8Array, root: ManagedDirectoryRoot, policy?: ManagedSessionSecurityPolicy): Promise<void>;
128
111
  /** Copy the exact bytes captured from one no-follow source descriptor. */
129
112
  export declare function copyManagedFileNoReplace(source: string, destination: string, snapshot?: ManagedFileSnapshot, root?: ManagedDirectoryRoot, policy?: ManagedSessionSecurityPolicy): Promise<void>;
130
113
  /** Acquire a lease lock with bounded wait, heartbeats, conservative stale reclaim, and fencing. */
131
114
  export declare function acquireManagedLock(locksDirectory: string, name: string, root?: ManagedDirectoryRoot, policy?: ManagedSessionSecurityPolicy): Promise<ManagedStorageLock>;
132
- /** Acquire a synchronous lease lock for a short cross-process critical section. */
133
- export declare function acquireManagedLockSync(locksDirectory: string, name: string, root?: ManagedDirectoryRoot, policy?: ManagedSessionSecurityPolicy): ManagedStorageLockSync;
134
115
  export interface ManagedArtifactTreeLimits {
135
116
  maxFiles?: number;
136
117
  maxTotalBytes?: number;
@@ -986,7 +986,7 @@ export declare class SessionManager {
986
986
  * @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
987
987
  */
988
988
  static open(filePath: string, destinationInput?: SessionDestinationInput, storage?: SessionStorage, migrationPolicy?: SessionDirectoryMigrationPolicy): Promise<SessionManager>;
989
- static openNestedManaged(filePath: string, destination: SessionDestination, store: ManagedSessionDescendantStore, storage?: SessionStorage): Promise<SessionManager>;
989
+ static openNestedManaged(filePath: string, destination: SessionDestination, store: ManagedSessionDescendantStore, storage?: SessionStorage, cwdOverride?: string): Promise<SessionManager>;
990
990
  /**
991
991
  * List default-managed sessions for the resume picker without recovery or other
992
992
  * maintenance writes. This is the only picker inventory that includes legacy
@@ -107,7 +107,7 @@ export interface ExecutorOptions {
107
107
  export declare class ManagedTaskPersistence {
108
108
  #private;
109
109
  constructor(artifacts: ArtifactManager, taskId: string);
110
- openSession(): Promise<SessionManager>;
110
+ openSession(cwd: string): Promise<SessionManager>;
111
111
  publishOutput(rawOutput: string, metadata: Uint8Array): Promise<void>;
112
112
  }
113
113
  export declare function createManagedTaskPersistence(artifacts: ArtifactManager, taskId: string): ManagedTaskPersistence;
@@ -5,8 +5,9 @@ import { type SubagentRunOutcome } from "../async";
5
5
  import type { Theme } from "../modes/theme/theme";
6
6
  import { type SingleResult, type TaskToolDetails, type TaskToolSchemaInstance } from "./types";
7
7
  import "../tools/review";
8
+ import { type TaskResultReceipt } from "./receipt";
8
9
  import { renderResult, renderCall as renderTaskCall } from "./render";
9
- export declare function subagentRunOutcomeFromSingleResult(finalText: string, singleResult: Pick<SingleResult, "aborted" | "exitCode" | "paused" | "setupFailure"> | undefined): string | SubagentRunOutcome;
10
+ export declare function subagentRunOutcomeFromSingleResult(finalText: string, singleResult: (Pick<SingleResult, "aborted" | "exitCode" | "paused" | "setupFailure"> & Partial<Pick<TaskResultReceipt, "status">>) | undefined): string | SubagentRunOutcome;
10
11
  export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
11
12
  export { discoverCommands, expandCommand, getCommand } from "./commands";
12
13
  export { discoverAgents, getAgent } from "./discovery";
@@ -34,6 +34,7 @@ export interface TaskResultReceipt {
34
34
  cost?: number;
35
35
  usageCostBreakdownComplete?: true;
36
36
  branchName?: string;
37
+ persistence?: SingleResult["persistence"];
37
38
  retryFailure?: {
38
39
  attempt: number;
39
40
  errorSummary: string;
@@ -495,6 +495,18 @@ export interface SetupFailureSummary {
495
495
  }
496
496
  /** Create a bounded diagnostic that preserves a setup failure's cause without exposing credentials or local paths. */
497
497
  export declare function createSetupFailureSummary(error: unknown): SetupFailureSummary;
498
+ export interface TaskRecoveryArtifactRef {
499
+ uri: string;
500
+ sizeBytes: number;
501
+ sha256: string;
502
+ /** Recovery artifacts remain readable for the parent session lifetime. */
503
+ durability: "session";
504
+ }
505
+ export interface TaskPersistenceResult {
506
+ outcome: "applied" | "no_changes" | "recovery_available";
507
+ ownerWorktreeApplied: boolean;
508
+ recoveryRef?: TaskRecoveryArtifactRef;
509
+ }
498
510
  /** Result from a single agent execution */
499
511
  export interface SingleResult {
500
512
  index: number;
@@ -540,6 +552,10 @@ export interface SingleResult {
540
552
  nestedPatches?: NestedRepoPatch[];
541
553
  /** Whether isolated execution produced a non-empty root or nested patch. */
542
554
  producedChanges?: boolean;
555
+ /** Receipt-safe owner-worktree persistence result for isolated execution. */
556
+ persistence?: TaskPersistenceResult;
557
+ /** Identity-bound patch artifact captured before isolation cleanup. */
558
+ recoveryRef?: TaskRecoveryArtifactRef;
543
559
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
544
560
  extractedToolData?: Record<string, unknown[]>;
545
561
  /** Full wrapper-owned review evidence, kept separate from caller completion data. */
@@ -22,6 +22,8 @@ export interface WorktreeBaseline {
22
22
  export declare function getRepoRoot(cwd: string): Promise<string>;
23
23
  export declare function getGitNoIndexNullPath(): string;
24
24
  export declare function captureBaseline(repoRoot: string): Promise<WorktreeBaseline>;
25
+ /** Confirm that applying patches to the captured owner baseline exactly matches the current owner worktree. */
26
+ export declare function verifyRootPatchesApplied(repoRoot: string, baseline: WorktreeBaseline, patches: readonly string[]): Promise<boolean>;
25
27
  export interface NestedRepoPatch {
26
28
  relativePath: string;
27
29
  patch: string;
@@ -29,14 +31,19 @@ export interface NestedRepoPatch {
29
31
  export interface DeltaPatchResult {
30
32
  rootPatch: string;
31
33
  nestedPatches: NestedRepoPatch[];
34
+ captureErrors?: string[];
32
35
  }
36
+ export interface RecoveryPatchBundle {
37
+ version: 1;
38
+ rootPatch: string;
39
+ nestedPatches: NestedRepoPatch[];
40
+ captureErrors?: string[];
41
+ }
42
+ export declare function serializeRecoveryPatchBundle(delta: DeltaPatchResult): string;
43
+ export declare function verifyNestedPatchesApplied(repoRoot: string, baseline: WorktreeBaseline, patches: readonly NestedRepoPatch[]): Promise<boolean>;
33
44
  export declare function captureDeltaPatch(isolationDir: string, baseline: WorktreeBaseline): Promise<DeltaPatchResult>;
34
- /**
35
- * Apply nested repo patches directly to their working directories after parent merge.
36
- * @param commitMessage Optional async function to generate a commit message from the combined diff.
37
- * If omitted or returns null, falls back to a generic message.
38
- */
39
- export declare function applyNestedPatches(repoRoot: string, patches: NestedRepoPatch[], commitMessage?: (diff: string) => Promise<string | null>): Promise<void>;
45
+ /** Preflight every nested delta, then roll back earlier repositories if a later apply fails. */
46
+ export declare function applyNestedPatches(repoRoot: string, patches: NestedRepoPatch[]): Promise<void>;
40
47
  /**
41
48
  * User-facing isolation mode names exposed by the `task.isolation.mode`
42
49
  * setting. Mapped to a backend-kind hint via {@link parseIsolationMode};
@@ -57,6 +57,8 @@ export interface SubagentSnapshot {
57
57
  requestedModel?: string;
58
58
  /** True when the requested model lacked credentials and fell back to the parent model. */
59
59
  modelFellBack?: boolean;
60
+ /** True when the effective subagent provider is in fast mode. */
61
+ fastMode?: boolean;
60
62
  }
61
63
  export type SubagentAwaitOutcome = "completed" | "timed_out" | "interrupted";
62
64
  export interface SubagentToolDetails {
@@ -72,6 +72,7 @@ export interface PushOptions {
72
72
  export interface PatchOptions {
73
73
  readonly cached?: boolean;
74
74
  readonly check?: boolean;
75
+ readonly reverse?: boolean;
75
76
  readonly env?: Record<string, string | undefined>;
76
77
  readonly signal?: AbortSignal;
77
78
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.12.2",
4
+ "version": "0.12.5",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -53,13 +53,13 @@
53
53
  "@agentclientprotocol/sdk": "1.3.0",
54
54
  "@babel/parser": "^7.29.3",
55
55
  "@mozilla/readability": "^0.6.0",
56
- "@gajae-code/stats": "0.12.2",
57
- "@gajae-code/agent-core": "0.12.2",
58
- "@gajae-code/ai": "0.12.2",
59
- "@gajae-code/bridge-client": "0.12.2",
60
- "@gajae-code/natives": "0.12.2",
61
- "@gajae-code/tui": "0.12.2",
62
- "@gajae-code/utils": "0.12.2",
56
+ "@gajae-code/stats": "0.12.5",
57
+ "@gajae-code/agent-core": "0.12.5",
58
+ "@gajae-code/ai": "0.12.5",
59
+ "@gajae-code/bridge-client": "0.12.5",
60
+ "@gajae-code/natives": "0.12.5",
61
+ "@gajae-code/tui": "0.12.5",
62
+ "@gajae-code/utils": "0.12.5",
63
63
  "@puppeteer/browsers": "^2.13.0",
64
64
  "@types/turndown": "5.0.6",
65
65
  "@xterm/headless": "^6.0.0",
@@ -164,6 +164,8 @@ export interface SubagentRecord {
164
164
  effectiveModel?: string;
165
165
  /** True when the requested model lacked credentials and the subagent fell back to the parent model. */
166
166
  modelFellBack?: boolean;
167
+ /** True when the effective subagent provider is in fast mode. */
168
+ fastMode?: boolean;
167
169
  }
168
170
 
169
171
  /** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
@@ -702,16 +704,22 @@ export class AsyncJobManager {
702
704
  this.#notifyChange();
703
705
  }
704
706
 
705
- /** Patch model metadata onto an existing subagent record (best-effort; no-op if unknown). */
707
+ /**
708
+ * Patch model/runtime metadata onto an existing subagent record (best-effort; no-op if
709
+ * unknown). Every field is optional and omitting one preserves its current value, so a
710
+ * narrow patch like `{ fastMode: true }` cannot erase model identity recorded earlier.
711
+ * A field therefore cannot be cleared back to `undefined` through this method.
712
+ */
706
713
  updateSubagentModel(
707
714
  subagentId: string,
708
- model: { requestedModel?: string; effectiveModel?: string; modelFellBack?: boolean },
715
+ model: { requestedModel?: string; effectiveModel?: string; modelFellBack?: boolean; fastMode?: boolean },
709
716
  ): void {
710
717
  const record = this.#subagentRecords.get(subagentId);
711
718
  if (!record) return;
712
- record.requestedModel = model.requestedModel;
713
- record.effectiveModel = model.effectiveModel;
714
- record.modelFellBack = model.modelFellBack;
719
+ record.requestedModel = model.requestedModel ?? record.requestedModel;
720
+ record.effectiveModel = model.effectiveModel ?? record.effectiveModel;
721
+ record.modelFellBack = model.modelFellBack ?? record.modelFellBack;
722
+ record.fastMode = model.fastMode ?? record.fastMode;
715
723
  }
716
724
 
717
725
  #recordFromResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
@@ -53,6 +53,7 @@ export const FAMOUS_PROVIDER_ORDER: readonly string[] = [
53
53
  "xiaomi-token-plan-cn",
54
54
  "opengateway",
55
55
  "bizrouter",
56
+ "mara",
56
57
  "github-copilot",
57
58
  "cursor",
58
59
  ];
@@ -230,9 +230,22 @@ Team remains explicit and separate: Team is not auto-launched, not a hidden pipe
230
230
 
231
231
  Heavyweight review runs **once per boundary**, not once per story. In aggregate mode the whole required-goal set is one implicit boundary by default: every checkpoint before the run's final required goal may present the lightweight `deferredToBatch` gate, and only the final goal carries the full strict gate. Nothing needs to be declared to get this — it is the default.
232
232
 
233
- Per-subgoal verification is **agent-chosen**. The runtime does not dictate which lanes an intermediate story runs; it enforces only that the declaration matches the evidence. List the lanes you actually ran in `deferredToBatch.ranLanes` (`targetedVerification`, and optionally `aiSlopCleaner` / `iteration`). A declared lane with no evidence is rejected, submitted evidence for an undeclared lane is rejected, and `ranLanes` can never claim `architectReview` or `executorQa` — a deferred gate structurally cannot carry review evidence, so those lanes always belong to the boundary. `targetedVerification` remains mandatory; the ai-slop-cleaner pass and a full verification rerun are boundary duties, not per-subgoal duties.
233
+ A deferred gate is just the proof the runtime cannot know: that targeted verification ran. Everything mechanical — `kind`, the batch tuple, `deferredLanes`, and the whole `changeSet` block (`paths`, `changeSetHash`) — is auto-filled from durable state and the computed cumulative git diff. Never hand-compute a hash. The minimal valid gate:
234
234
 
235
- Deferring never manufactures approvals: a `deferredToBatch` gate still cannot contain `architectReview`, `executorQa`, or `validationBatchClose`, and the boundary gate keeps every existing fail-closed rule.
235
+ ```json
236
+ {
237
+ "deferredToBatch": {
238
+ "ranLanes": ["targetedVerification"],
239
+ "targetedVerification": {
240
+ "status": "passed",
241
+ "commands": ["bun test <targeted suite>"],
242
+ "evidence": "what was verified and how it passed"
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ `deferredToBatch.ranLanes` lists the lanes you actually ran (`targetedVerification`, plus optionally `aiSlopCleaner` / `iteration`); declaration and evidence must match in both directions. `ranLanes` can never claim `architectReview` or `executorQa`, and a deferred gate can never contain `architectReview`, `executorQa`, or `validationBatchClose` — review always belongs to the boundary, and deferring never manufactures approvals. Any optional field you do supply must match reality; a wrong value fails closed. Check with `gjc ultragoal quality-gate validate` before checkpointing.
236
249
 
237
250
  ### Validation batches (explicit phase/module boundaries)
238
251
 
@@ -247,7 +260,7 @@ gjc ultragoal create-goals --brief-file <path> --validation-batch-json '[{"schem
247
260
  Checkpoint contract summary — the full contract lives in the `validation-batch-contracts` fragment (`skill-fragments/ultragoal/validation-batch-contracts.md`); load it before checkpointing any batch member:
248
261
 
249
262
  - **Non-final members** checkpoint `complete` with a single top-level `deferredToBatch` quality gate (kind `validation-batch-deferred`) proving targeted verification, a declaration-matched lane set, and a cumulative-since-base change set — never `architectReview`, `executorQa`, or `validationBatchClose`; deferring never manufactures fake review approvals.
250
- - **The final member** (`finalGoalId`) checkpoints `complete` with the normal full strict gate PLUS a top-level `validationBatchClose` proof covering all members; out-of-order close is rejected, close state is append-only proof on the final member only, and batch invalidation is fail-closed.
263
+ - **The final member** (`finalGoalId`) checkpoints `complete` with the normal full strict gate PLUS a top-level `validationBatchClose` proof covering all members; out-of-order close is rejected, close state is append-only proof on the final member only, and batch invalidation is fail-closed. Like the deferred gate, every close field except `coverageEvidence` is auto-filled from durable receipts and the computed diff — the minimal close is `{"validationBatchClose":{"coverageEvidence":"..."}}` alongside the strict gate.
251
264
 
252
265
  ### Intra-goal validation-lane parallelism
253
266
 
@@ -291,7 +304,7 @@ One generation freezes the change set and reviews it exactly once:
291
304
  5. Delegate an `executor` QA/red-team lane with typed `executionMode: "ultragoal-red-team"` (preferred) — or assignment text that explicitly labels Ultragoal completion QA/red-team — to build and run the e2e/red-teaming QA suite appropriate for the story. A bare `executorQa` field-name mention is not enough to activate the mode. This lane must try to break the change, not just confirm the happy path. It must start from the approved plan/spec/acceptance criteria, then user-facing contracts, and only then implementation code as supporting evidence. Plan/code mismatches are blockers, not items to paper over with implementation intent.
292
305
  6. The executor QA/red-team lane must prove evidence by the real surface under test:
293
306
  - GUI/web surfaces require a valid automation transcript plus a non-uniform screenshot. Bare `inlineEvidence` text or typed receipts never prove live GUI/web execution.
294
- - CLI surfaces require runtime argv replay: `schemaVersion: 1`, `kind: "cli-replay"`, `replaySafe: true`, an argv `command` (any executable — no command allowlist), and replayed output validation. The complete field-by-field replay schema, safe `env` constraints, and `replayExempt` audit contract are specified once in the "For CLI replay artifacts" paragraph below the quality-gate JSON; follow it exactly.
307
+ - CLI surfaces require a safe runtime argv replay (`schemaVersion: 1`, `kind: "cli-replay"`, `replaySafe: true`) or the existing audited `replayExempt` path with a screenshot, automation, or PTY structural fallback. Runtime replay is limited to the pinned Bun runtime for `bun --version` or literal `bun -e "console.log(...)"`; the gate never executes model-authored test files. Shells, interpreters with code strings, path-qualified executables, package/git/network mutation commands, `bun test`, and arbitrary argv are rejected. Structured `test-report` fallback remains unsupported pending a separately reviewed provenance design.
295
308
  - Native/desktop/tui surfaces require a structurally valid screenshot, PTY capture with terminal control codes, or app-automation transcript.
296
309
  - API/package surfaces require a real artifact file or typed receipt whose artifact `kind` contains one of `api`, `package`, `consumer`, `black-box`, or `test-report`; examples: `api-package-test-report`, `package-consumer-report`, `black-box-api-receipt`. Algorithm/math surfaces require a real artifact file or typed receipt whose artifact `kind` contains one of `property`, `boundary`, `edge`, `adversarial`, `failure`, `math`, `algorithm`, or `test-report`; examples: `property-test-report`, `algorithm-boundary-report`. Bare `inlineEvidence` text alone is not sufficient for any surface.
297
310
  - The mandatory **computer-use** red-team suite (`kill-switch-bypass`, `suspended-enforcement`, `permission-revoked`, …) is conditional, not universal: require it only when computer/desktop control is genuinely part of the product surface being dogfooded. For every other product type, prove the change through the matching live surface instead — browser-use automation for web/GUI, bash/CLI live invocation or argv replay for CLI, and real artifacts or typed receipts for API/package/algorithm/math. Editing docs, prompts, or skills that merely mention computer-use does not by itself make the computer-use suite applicable; pick the red-team surface that matches what the change actually ships.
@@ -364,7 +377,11 @@ Validate before you checkpoint. `gjc ultragoal quality-gate validate --quality-g
364
377
 
365
378
  Provide one `artifactRefs` entry per live surface actually exercised, using the surface-appropriate `kind` and evidence rules from steps 6–7 above; the CLI rejects missing or shallow gates. `status: "not_applicable"` rows are allowed only in `contractCoverage` and `surfaceEvidence` and each requires `contractRef` plus `reason`.
366
379
 
367
- For CLI replay artifacts, the JSON at `path` must be an object like `{"schemaVersion":1,"kind":"cli-replay","replaySafe":true,"command":["bun","-e","console.log(\"ultragoal-cli-ok\")"],"cwd":".","env":{"LC_ALL":"C"},"timeoutMs":30000,"expectedExitCode":0,"recordedStdout":"ultragoal-cli-ok\n","recordedStderr":"","invariants":[{"type":"substring","value":"ultragoal-cli-ok"},{"type":"not-substring","value":"error"}]}`. Accepted replay fields are `command` (string array — any executable is allowed; there is no command allowlist), optional `cwd`, safe `env`, `timeoutMs`, `expectedExitCode`, `recordedStdout`, `recordedStderr`, `normalization`, and `invariants`. The replay sandbox already isolates the command as a subprocess with a timeout, 1 MiB output cap, and env scrubbing, so `replaySafe: true` is the gating attestation rather than an executable allowlist. `env` must contain only safe deterministic variables (`LANG`, `LC_ALL`, `LC_CTYPE`, `TZ`), never credentials or machine/user-specific secrets. `normalization` is optional and, when provided, must be exactly the string `"default"` (the built-in normalizer already strips ANSI codes, normalizes line endings, scrubs paths, and trims trailing whitespace); object-shaped normalization is rejected. Invariants may be substring, regex, or not-substring checks; when present, they replace exact `recordedStdout` equality — without `invariants`, replayed normalized stdout must match `recordedStdout` exactly. Commands that cannot be replayed deterministically (non-zero side effects, require credentials, require network, interactive, platform-unavailable, destructive) may instead use audited `replayExempt` metadata with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs` plus a structurally valid same-surface fallback artifact. `reason` must be substantive and audited, and `approvedBy` must identify the verifier. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
380
+ For safe CLI replay artifacts, the JSON at `path` must be an object like `{"schemaVersion":1,"kind":"cli-replay","replaySafe":true,"command":["bun","-e","console.log(\"ultragoal-cli-ok\")"],"cwd":".","env":{"LC_ALL":"C"},"timeoutMs":30000,"expectedExitCode":0,"recordedStdout":"ultragoal-cli-ok\n","recordedStderr":"","invariants":[{"type":"substring","value":"ultragoal-cli-ok"},{"type":"not_substring","value":"error"}]}`. `replaySafe: true` is required but is never authority by itself: executable replay is limited to the pinned Bun runtime for `bun --version` or deterministic literal `bun -e "console.log(...)"`. Shells, nested interpreters, path-qualified executables, test source, install/publish commands, git mutation, network clients, and every other argv are rejected. The declared cwd and artifact files are realpath-confined beneath the repository, but the safe probe itself runs from a fresh empty temporary cwd/home so repository `bunfig.toml` preloads and user configuration cannot execute. Mixed inline/nested/file-backed rows fail closed, POSIX timeout cleanup signals the replay process group, stdout and stderr are validated after normalization, output is capped at 1 MiB, and the child environment is scrubbed to `CI`, `NO_COLOR`, `GJC_ULTRAGOAL_REPLAY`, trusted temporary `HOME`/`TMPDIR`, plus optional `LANG`, `LC_ALL`, `LC_CTYPE`, and `TZ`.
381
+
382
+ Compiled GJC binaries fail executable replay closed because their `process.execPath` launches GJC rather than a Bun CLI. Those runs must use the existing audited `replayExempt` structural fallback; the validator never resolves an untrusted `bun` from `PATH`.
383
+
384
+ Focused `bun test` execution is blocked because repository test source is still arbitrary host code without an operating-system sandbox. The current `replayExempt` contract continues to require an existing screenshot, automation transcript, or PTY structural fallback; a `test-report` or `bun-test-report` JSON file is intentionally not accepted yet. Keep that design work open until test-result provenance, output binding, and consumer authority can be made fail-closed. Allowed `reasonCode` values remain `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
368
385
 
369
386
  ## Terminal critic gate
370
387
 
@@ -6,7 +6,8 @@ Internal Ultragoal sub-skill fragment (`kind: "skill-fragment"`, parent skill `u
6
6
 
7
7
  - **Non-final members** checkpoint `complete` with a single top-level `deferredToBatch` quality gate (kind `validation-batch-deferred`): a passing `targetedVerification` lane plus a cumulative-since-base change set. An ai-slop-cleaner pass and a full verification rerun are boundary duties and are optional here; when either is supplied it must pass and be blocker-free. A `deferredToBatch` gate must NOT contain `architectReview`, `executorQa`, or `validationBatchClose` — deferring never manufactures fake review approvals.
8
8
  - **Lane declaration is fail-closed.** `deferredToBatch.ranLanes` declares which lanes actually ran. A declared lane whose evidence is missing is rejected, evidence for an undeclared lane is rejected, `targetedVerification` must always be declared when `ranLanes` is present, and `ranLanes` can never declare `architectReview` or `executorQa`. Omitting `ranLanes` is allowed only for a gate that carries the mandatory targeted lane alone.
9
- - **The final member** (`finalGoalId`) checkpoints `complete` with the normal full strict gate PLUS a top-level `validationBatchClose` proof that covers all member IDs, member metadata hashes, member receipt/checkpoint-ledger-event IDs, per-member change-set hashes, and union change-set coverage. The final close only starts once every non-final member is already `complete` with a structurally fresh deferred receipt (out-of-order close is rejected).
9
+ - **Derivable fields are auto-filled — never hand-compute a hash.** When omitted, the runtime fills `kind`, the batch tuple, `deferredLanes`, and the whole `changeSet` block (`memberGoalId`, `cumulativeFromBase`, `paths` from the computed cumulative diff, `changeSetHash`); the minimal deferred gate is `{"deferredToBatch":{"targetedVerification":{"status":"passed","commands":["..."],"evidence":"..."}}}`. A supplied value must still match reality; `changeSet.paths` rows may be plain path strings or `{path, status}` objects.
10
+ - **The final member** (`finalGoalId`) checkpoints `complete` with the normal full strict gate PLUS a top-level `validationBatchClose` proof that covers all member IDs, member metadata hashes, member receipt/checkpoint-ledger-event IDs, per-member change-set hashes, and union change-set coverage. Every close field except `coverageEvidence` is auto-filled from durable receipts and the computed cumulative diff when omitted — the minimal close is `{"validationBatchClose":{"coverageEvidence":"..."}}` alongside the strict gate, and a supplied value must still match durable state. The final close only starts once every non-final member is already `complete` with a structurally fresh deferred receipt (out-of-order close is rejected).
10
11
  - Close state is append-only proof: it lives in the final member's checkpoint receipt and matching `goal_checkpointed` ledger row only. Never stamp `closedReceiptId`/`closedAt` or any close-state field onto member goals, and never append a separate close ledger event.
11
12
  - Change sets are cumulative-since-base: each member's `changeSet.paths` is the whole-worktree diff vs base (`cumulativeFromBase: true`), `memberGoalId` is a label not a per-path attribution, and `unionChangeSet.paths` carries no per-goal attribution.
12
13
  - Batch invalidation is fail-closed: steering mutations that would invalidate a batch are rejected while any member holds a fresh deferred receipt.