@gajae-code/coding-agent 0.11.4 → 0.11.6

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 (154) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +31 -1
  4. package/dist/types/export/html/template.generated.d.ts +1 -1
  5. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  7. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  8. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  9. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  10. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  12. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  13. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  14. package/dist/types/main.d.ts +2 -2
  15. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  16. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  17. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
  20. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  21. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  24. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  25. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  26. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  27. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  28. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  29. package/dist/types/sdk/bus/chat-daemon-control.d.ts +42 -4
  30. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  31. package/dist/types/sdk/bus/config.d.ts +22 -0
  32. package/dist/types/sdk/bus/index.d.ts +162 -2
  33. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  34. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  35. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  36. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -3
  37. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  38. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  39. package/dist/types/sdk/session.d.ts +16 -1
  40. package/dist/types/secrets/index.d.ts +4 -2
  41. package/dist/types/secrets/obfuscator.d.ts +1 -1
  42. package/dist/types/session/agent-session.d.ts +31 -2
  43. package/dist/types/session/artifacts.d.ts +30 -8
  44. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  45. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  46. package/dist/types/session/session-manager.d.ts +98 -18
  47. package/dist/types/session/session-storage.d.ts +27 -0
  48. package/dist/types/session/streaming-output.d.ts +4 -0
  49. package/dist/types/session/yield-queue.d.ts +8 -0
  50. package/dist/types/task/executor.d.ts +9 -0
  51. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  52. package/dist/types/tools/ask.d.ts +48 -0
  53. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  54. package/dist/types/tools/index.d.ts +4 -0
  55. package/package.json +9 -9
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  57. package/src/cli/notify-cli.ts +40 -18
  58. package/src/commands/sdk.ts +7 -2
  59. package/src/config/settings-schema.ts +31 -1
  60. package/src/daemon/runtime.ts +2 -2
  61. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  63. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  64. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  65. package/src/eval/py/index.ts +2 -2
  66. package/src/export/html/template.generated.ts +1 -1
  67. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  68. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  69. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  70. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  71. package/src/gjc-runtime/launch-tmux.ts +61 -6
  72. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  73. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  74. package/src/gjc-runtime/state-runtime.ts +38 -12
  75. package/src/gjc-runtime/team-runtime.ts +1 -1
  76. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  77. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  78. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  79. package/src/harness-control-plane/owner.ts +59 -3
  80. package/src/hindsight/client.ts +65 -2
  81. package/src/internal-urls/agent-protocol.ts +76 -5
  82. package/src/internal-urls/docs-index.generated.ts +8 -7
  83. package/src/internal-urls/local-protocol.ts +416 -18
  84. package/src/lsp/index.ts +4 -88
  85. package/src/main.ts +36 -12
  86. package/src/modes/acp/acp-agent.ts +8 -0
  87. package/src/modes/components/ansi-display-validator.ts +54 -0
  88. package/src/modes/components/notifications-settings-editor.ts +26 -2
  89. package/src/modes/components/session-observer-overlay.ts +40 -24
  90. package/src/modes/components/settings-selector.ts +3 -1
  91. package/src/modes/components/tool-transcript-format.ts +321 -0
  92. package/src/modes/components/transcript-viewer-overlay.ts +39 -17
  93. package/src/modes/controllers/command-controller.ts +15 -0
  94. package/src/modes/controllers/extension-ui-controller.ts +14 -2
  95. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  96. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  97. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  98. package/src/modes/controllers/selector-controller.ts +70 -8
  99. package/src/modes/interactive-mode.ts +1 -0
  100. package/src/modes/runtime-init.ts +16 -2
  101. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  102. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  103. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  104. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  105. package/src/plan-mode/approved-plan.ts +3 -1
  106. package/src/prompts/agents/architect.md +1 -0
  107. package/src/prompts/agents/critic.md +1 -0
  108. package/src/prompts/system/custom-system-prompt.md +1 -1
  109. package/src/prompts/system/system-prompt.md +2 -1
  110. package/src/prompts/tools/lsp.md +3 -3
  111. package/src/runtime-mcp/manager.ts +16 -1
  112. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  113. package/src/sdk/broker/session-index.ts +122 -64
  114. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  115. package/src/sdk/bus/chat-daemon-control.ts +538 -130
  116. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  117. package/src/sdk/bus/config-commands.ts +22 -0
  118. package/src/sdk/bus/config.ts +37 -0
  119. package/src/sdk/bus/index.ts +388 -94
  120. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  121. package/src/sdk/bus/notification-service.ts +284 -62
  122. package/src/sdk/bus/recent-activity.ts +14 -5
  123. package/src/sdk/bus/session-control.ts +150 -44
  124. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  125. package/src/sdk/bus/telegram-daemon-contract.ts +11 -3
  126. package/src/sdk/bus/telegram-daemon-control.ts +312 -76
  127. package/src/sdk/bus/telegram-daemon.ts +1916 -285
  128. package/src/sdk/host/control/dispatch.ts +14 -5
  129. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  130. package/src/sdk/session.ts +155 -27
  131. package/src/secrets/index.ts +53 -10
  132. package/src/secrets/obfuscator.ts +21 -18
  133. package/src/secrets/regex.ts +2 -0
  134. package/src/session/agent-session.ts +1447 -852
  135. package/src/session/artifacts.ts +188 -23
  136. package/src/session/context-estimation.ts +7 -2
  137. package/src/session/internal/managed-session-scope.ts +200 -38
  138. package/src/session/internal/managed-session-storage.ts +1152 -46
  139. package/src/session/session-manager.ts +1121 -148
  140. package/src/session/session-storage.ts +109 -11
  141. package/src/session/streaming-output.ts +6 -0
  142. package/src/session/yield-queue.ts +20 -0
  143. package/src/slash-commands/builtin-registry.ts +36 -0
  144. package/src/task/executor.ts +71 -26
  145. package/src/task/index.ts +63 -20
  146. package/src/tool-discovery/tool-index.ts +18 -0
  147. package/src/tools/ask-answer-registry.ts +15 -1
  148. package/src/tools/ask.ts +129 -32
  149. package/src/tools/bash-skill-urls.ts +14 -3
  150. package/src/tools/bash.ts +2 -2
  151. package/src/tools/index.ts +4 -0
  152. package/src/tools/plan-mode-guard.ts +1 -0
  153. package/src/tools/sqlite-reader.ts +3 -2
  154. package/src/tools/subagent.ts +2 -0
@@ -12,12 +12,11 @@ import * as path from "node:path";
12
12
 
13
13
  import { isEnoent, logger, pathIsWithin } from "@gajae-code/utils";
14
14
 
15
- import { cachePlugin } from "./cache";
15
+ import { cachePlugin, getCachedPluginPath } from "./cache";
16
16
  import { classifySource, fetchMarketplace, parseMarketplaceCatalog, promoteCloneToCache } from "./fetcher";
17
17
  import {
18
18
  addInstalledPlugin,
19
19
  addMarketplaceEntry,
20
- collectReferencedPaths,
21
20
  getInstalledPlugin,
22
21
  getMarketplaceEntry,
23
22
  readInstalledPluginsRegistry,
@@ -255,6 +254,7 @@ export class MarketplaceManager {
255
254
  if (existing && existing.length > 0 && !force) {
256
255
  throw new Error(`Plugin "${pluginId}" is already installed. Use force option to reinstall.`);
257
256
  }
257
+ const existingCachePaths = existing ? await this.#validateCacheDeletionTargets(pluginId, existing) : [];
258
258
 
259
259
  // 4. Resolve source path.
260
260
  // marketplaceClonePath is the marketplace root — the directory containing .Anthropic model-plugin/
@@ -289,6 +289,9 @@ export class MarketplaceManager {
289
289
  let cachePath!: string;
290
290
  try {
291
291
  version = await this.#resolvePluginVersion(pluginEntry, sourcePath);
292
+ if ([marketplace, name, version].some(component => /[. ]$/.test(component))) {
293
+ throw new Error(`Invalid cache identity for plugin "${pluginId}"`);
294
+ }
292
295
  cachePath = await cachePlugin(sourcePath, this.#opts.pluginsCacheDir, marketplace, name, version);
293
296
  await this.#writeEmbeddedLspConfig(pluginEntry, cachePath);
294
297
  } finally {
@@ -311,11 +314,13 @@ export class MarketplaceManager {
311
314
  ? readInstalledPluginsRegistry(this.#opts.projectInstalledRegistryPath)
312
315
  : Promise.resolve({ version: 2 as const, plugins: {} as Record<string, InstalledPluginEntry[]> }),
313
316
  ]);
314
- const referenced = collectReferencedPaths(userReg, projectReg);
317
+ const referenced = this.#collectReferencedCacheIdentities(userReg, projectReg);
318
+ const cachePathIdentity = this.#cacheDeletionIdentity(cachePath);
315
319
 
316
- for (const entry of existing) {
317
- if (entry.installPath !== cachePath && !referenced.has(entry.installPath)) {
318
- await fs.rm(entry.installPath, { recursive: true, force: true });
320
+ for (const existingCachePath of existingCachePaths) {
321
+ const existingIdentity = this.#cacheDeletionIdentity(existingCachePath);
322
+ if (existingIdentity !== cachePathIdentity && !referenced.has(existingIdentity)) {
323
+ await fs.rm(existingCachePath, { recursive: true, force: true });
319
324
  }
320
325
  }
321
326
  }
@@ -395,6 +400,84 @@ export class MarketplaceManager {
395
400
  return "0.0.0";
396
401
  }
397
402
 
403
+ async #validateCacheDeletionTargets(pluginId: string, entries: readonly InstalledPluginEntry[]): Promise<string[]> {
404
+ const parsed = parsePluginId(pluginId);
405
+ if (!parsed) {
406
+ throw new Error(`Invalid plugin ID format: "${pluginId}". Expected "name@marketplace".`);
407
+ }
408
+
409
+ const targets: string[] = [];
410
+ for (const entry of entries) {
411
+ if ([parsed.marketplace, parsed.name, entry.version].some(component => /[. ]$/.test(component))) {
412
+ throw new Error(`Refusing to remove plugin "${pluginId}": invalid recorded cache identity`);
413
+ }
414
+
415
+ let targetPath: string;
416
+ try {
417
+ targetPath = getCachedPluginPath(
418
+ this.#opts.pluginsCacheDir,
419
+ parsed.marketplace,
420
+ parsed.name,
421
+ entry.version,
422
+ );
423
+ } catch (error) {
424
+ throw new Error(`Refusing to remove plugin "${pluginId}": invalid recorded cache identity`, {
425
+ cause: error,
426
+ });
427
+ }
428
+
429
+ if (entry.installPath !== targetPath) {
430
+ throw new Error(`Refusing to remove plugin "${pluginId}": recorded install path is not its cache path`);
431
+ }
432
+
433
+ try {
434
+ const stat = await fs.lstat(targetPath);
435
+ if (stat.isSymbolicLink()) {
436
+ throw new Error(`Refusing to remove plugin "${pluginId}": cache path is a symbolic link or junction`);
437
+ }
438
+ } catch (error) {
439
+ if (!isEnoent(error)) throw error;
440
+ }
441
+
442
+ targets.push(targetPath);
443
+ }
444
+ return targets;
445
+ }
446
+
447
+ #cacheDeletionIdentity(cachePath: string): string {
448
+ // Model Windows path identity on every host so reference protection is
449
+ // deterministic: case is ignored and trailing dots/spaces are discarded.
450
+ return path
451
+ .resolve(cachePath)
452
+ .split(path.sep)
453
+ .map(component => component.replace(/[. ]+$/, "").toLowerCase())
454
+ .join(path.sep);
455
+ }
456
+
457
+ #collectReferencedCacheIdentities(...registries: InstalledPluginsRegistry[]): Set<string> {
458
+ const identities = new Set<string>();
459
+ for (const registry of registries) {
460
+ for (const [pluginId, entries] of Object.entries(registry.plugins)) {
461
+ const parsed = parsePluginId(pluginId);
462
+ if (!parsed) continue;
463
+ for (const entry of entries) {
464
+ try {
465
+ const cachePath = getCachedPluginPath(
466
+ this.#opts.pluginsCacheDir,
467
+ parsed.marketplace,
468
+ parsed.name,
469
+ entry.version,
470
+ );
471
+ identities.add(this.#cacheDeletionIdentity(cachePath));
472
+ } catch {
473
+ // Invalid registry entries cannot identify a permitted cache target.
474
+ }
475
+ }
476
+ }
477
+ }
478
+ return identities;
479
+ }
480
+
398
481
  async uninstallPlugin(pluginId: string, scope?: "user" | "project"): Promise<void> {
399
482
  const parsed = parsePluginId(pluginId);
400
483
  if (!parsed) {
@@ -434,6 +517,7 @@ export class MarketplaceManager {
434
517
  const targetEntries = targetScope === "project" ? projectEntries! : userEntries!;
435
518
  const targetReg = targetScope === "project" ? projectReg : userReg;
436
519
  const registryPath = this.#registryPath(targetScope);
520
+ const targetCachePaths = await this.#validateCacheDeletionTargets(pluginId, targetEntries);
437
521
 
438
522
  const updatedReg = removeInstalledPlugin(targetReg, pluginId);
439
523
  await writeInstalledPluginsRegistry(registryPath, updatedReg);
@@ -445,11 +529,11 @@ export class MarketplaceManager {
445
529
  ? readInstalledPluginsRegistry(this.#opts.projectInstalledRegistryPath)
446
530
  : Promise.resolve({ version: 2 as const, plugins: {} as Record<string, InstalledPluginEntry[]> }),
447
531
  ]);
448
- const referenced = collectReferencedPaths(freshUserReg, freshProjectReg);
532
+ const referenced = this.#collectReferencedCacheIdentities(freshUserReg, freshProjectReg);
449
533
 
450
- for (const entry of targetEntries) {
451
- if (!referenced.has(entry.installPath)) {
452
- await fs.rm(entry.installPath, { recursive: true, force: true });
534
+ for (const targetCachePath of targetCachePaths) {
535
+ if (!referenced.has(this.#cacheDeletionIdentity(targetCachePath))) {
536
+ await fs.rm(targetCachePath, { recursive: true, force: true });
453
537
  }
454
538
  }
455
539
 
@@ -8,7 +8,9 @@ import {
8
8
  } from "./deep-interview-ambiguity";
9
9
  import {
10
10
  answerHash,
11
+ assertDeepInterviewInputWithinLimit,
11
12
  assertDeepInterviewIntentManifest,
13
+ assertDeepInterviewStructuredResponseWithinLimit,
12
14
  createDeepInterviewIntentManifest,
13
15
  type DeepInterviewEstablishedFact,
14
16
  type DeepInterviewIntentItem,
@@ -17,6 +19,7 @@ import {
17
19
  type DeepInterviewStateEnvelope,
18
20
  type DeepInterviewTriggerMetadata,
19
21
  deriveRoundKey,
22
+ MAX_USER_RESPONSE_LENGTH,
20
23
  normalizeDeepInterviewEnvelope,
21
24
  questionHash,
22
25
  reviewDeepInterviewIntent,
@@ -426,6 +429,9 @@ export async function appendOrMergeDeepInterviewRound(
426
429
  input: DeepInterviewAnswerInput,
427
430
  options: { sessionId?: string } = {},
428
431
  ): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord; disputedFactIds?: string[] }> {
432
+ assertDeepInterviewStructuredResponseWithinLimit(input);
433
+ if (input.customInput !== undefined)
434
+ assertDeepInterviewInputWithinLimit(input.customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
429
435
  const envelope = await readEnvelope(statePath);
430
436
  const interviewId = input.interviewId ?? interviewIdOf(envelope);
431
437
  const shell = buildAnswerShell({
@@ -547,6 +553,7 @@ export async function enrichDeepInterviewRoundScoring(
547
553
  input: DeepInterviewScoringInput,
548
554
  options: { sessionId?: string } = {},
549
555
  ): Promise<{ record: DeepInterviewRoundRecord }> {
556
+ assertDeepInterviewStructuredResponseWithinLimit(input);
550
557
  const envelope = await readEnvelope(statePath);
551
558
  const interviewId = input.interviewId ?? interviewIdOf(envelope);
552
559
  const rounds = readRounds(envelope);
@@ -7,11 +7,14 @@ import { syncSkillActiveState } from "../skill-state/active-state";
7
7
  import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
8
8
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
9
9
  import {
10
+ assertDeepInterviewInputWithinLimit,
10
11
  assertDeepInterviewIntentReview,
11
12
  type DeepInterviewIntentCategory,
12
13
  type DeepInterviewIntentItem,
13
14
  type DeepInterviewIntentManifest,
14
15
  type DeepInterviewIntentReview,
16
+ MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
17
+ MAX_INITIAL_CONTEXT_LENGTH,
15
18
  normalizeDeepInterviewEnvelope,
16
19
  reviewDeepInterviewIntent,
17
20
  } from "./deep-interview-state";
@@ -559,6 +562,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
559
562
  ideaParts.push(arg);
560
563
  }
561
564
  const idea = ideaParts.join(" ").trim();
565
+ assertDeepInterviewInputWithinLimit(idea, MAX_INITIAL_CONTEXT_LENGTH, "initial_idea");
562
566
  const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
563
567
  const trace = hasFlag(args, "--trace") && idea ? await buildDeepInterviewTraceSummary(cwd, idea) : undefined;
564
568
  return {
@@ -632,6 +636,11 @@ export async function persistDeepInterviewSpec(
632
636
  cwd: string,
633
637
  resolved: ResolvedDeepInterviewSpecWriteArgs,
634
638
  ): Promise<PersistedDeepInterviewSpec> {
639
+ assertDeepInterviewInputWithinLimit(
640
+ resolved.spec,
641
+ MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
642
+ "structured deep-interview response",
643
+ );
635
644
  const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
636
645
  const existingRead = await readExistingStateForMutation(statePath);
637
646
  if (existingRead.kind === "corrupt" && !resolved.force) {
@@ -731,6 +740,7 @@ export async function persistDeepInterviewSpec(
731
740
 
732
741
  async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
733
742
  const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
743
+ assertDeepInterviewInputWithinLimit(resolved.idea, MAX_INITIAL_CONTEXT_LENGTH, "initial_idea");
734
744
  const now = new Date().toISOString();
735
745
  const payload: Record<string, unknown> = {
736
746
  active: true,
@@ -397,6 +397,126 @@ const MAX_INTENT_STATEMENT_LENGTH = 1_000;
397
397
  const MAX_INTENT_RATIONALE_LENGTH = 500;
398
398
  const MAX_INTENT_EVIDENCE_LENGTH = 80;
399
399
 
400
+ // =============================================================================
401
+ // Input validation: free-text field allowlist + size limits (ouroboros parity)
402
+ // =============================================================================
403
+
404
+ /**
405
+ * User-input fields that legitimately carry prose (goals, prompts, descriptions,
406
+ * answers). Shell metacharacters (`;`, `|`, `&`, backticks, `$()`) are valid prose
407
+ * here and must NOT be rejected as structural injection. Structural fields (ids,
408
+ * categories, hashes) stay strictly validated by their own guards.
409
+ */
410
+ export const DEEP_INTERVIEW_FREETEXT_FIELDS: ReadonlySet<string> = new Set([
411
+ "initial_context",
412
+ "initial_idea",
413
+ "initial_context_summary",
414
+ "user_response",
415
+ "answer",
416
+ "goal",
417
+ "objective",
418
+ "prompt",
419
+ "description",
420
+ "statement",
421
+ "restated_goal",
422
+ "evidence",
423
+ "excerpt",
424
+ ]);
425
+
426
+ /** DoS-prevention character-count caps, matching ask-schema string-length semantics. */
427
+ export const MAX_INITIAL_CONTEXT_LENGTH = 50_000;
428
+ export const MAX_USER_RESPONSE_LENGTH = 10_000;
429
+ /** Maximum serialized JavaScript-string length for one LLM-produced structured response. */
430
+ export const MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH = 100_000;
431
+
432
+ /** Count Unicode code points without allocating an intermediate character array. */
433
+ export function deepInterviewCharacterCount(value: string): number {
434
+ let count = 0;
435
+ for (const _character of value) count++;
436
+ return count;
437
+ }
438
+
439
+ export function isDeepInterviewFreeTextField(name: string): boolean {
440
+ return DEEP_INTERVIEW_FREETEXT_FIELDS.has(name);
441
+ }
442
+
443
+ /**
444
+ * Assert that one structured deep-interview response is JSON-serializable and
445
+ * bounded by JavaScript string length. This deliberately does not measure or
446
+ * cap accumulated persisted interview state.
447
+ */
448
+ export function assertDeepInterviewStructuredResponseWithinLimit(value: unknown): void {
449
+ let serialized: string | undefined;
450
+ try {
451
+ serialized = JSON.stringify(value, (_key, nestedValue: unknown) => {
452
+ if (
453
+ typeof nestedValue === "bigint" ||
454
+ typeof nestedValue === "function" ||
455
+ typeof nestedValue === "symbol" ||
456
+ (typeof nestedValue === "number" && !Number.isFinite(nestedValue))
457
+ )
458
+ throw new Error("invalid structured deep-interview response");
459
+ return nestedValue;
460
+ });
461
+ } catch {
462
+ throw new Error("invalid structured deep-interview response");
463
+ }
464
+ if (typeof serialized !== "string") throw new Error("invalid structured deep-interview response");
465
+ if (deepInterviewCharacterCount(serialized) > MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH)
466
+ throw new Error(
467
+ `structured deep-interview response exceeds max length ${MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH}`,
468
+ );
469
+ }
470
+
471
+ /**
472
+ * Assert a free-text input is within its size cap. Never inspects content for shell
473
+ * metacharacters — free-text fields accept prose verbatim; this only bounds length.
474
+ */
475
+ export function assertDeepInterviewInputWithinLimit(value: string, max: number, fieldName = "input"): void {
476
+ if (typeof value !== "string") throw new Error(`${fieldName} must be a string`);
477
+ if (deepInterviewCharacterCount(value) > max) throw new Error(`${fieldName} exceeds max length ${max}`);
478
+ }
479
+
480
+ /** Validate user-supplied deep-interview prose before an envelope is persisted. */
481
+ export function assertDeepInterviewEnvelopeInputLimits(envelope: Record<string, unknown>): void {
482
+ const state =
483
+ typeof envelope.state === "object" && envelope.state !== null && !Array.isArray(envelope.state)
484
+ ? (envelope.state as Record<string, unknown>)
485
+ : {};
486
+ for (const field of ["initial_idea", "initial_context", "initial_context_summary"] as const) {
487
+ const nestedValue = state[field];
488
+ if (nestedValue !== undefined)
489
+ assertDeepInterviewInputWithinLimit(nestedValue as string, MAX_INITIAL_CONTEXT_LENGTH, `state.${field}`);
490
+ const topLevelValue = envelope[field];
491
+ if (topLevelValue !== undefined)
492
+ assertDeepInterviewInputWithinLimit(topLevelValue as string, MAX_INITIAL_CONTEXT_LENGTH, field);
493
+ }
494
+ for (const field of ["user_response", "answer"] as const) {
495
+ const nestedValue = state[field];
496
+ if (nestedValue !== undefined)
497
+ assertDeepInterviewInputWithinLimit(nestedValue as string, MAX_USER_RESPONSE_LENGTH, `state.${field}`);
498
+ const topLevelValue = envelope[field];
499
+ if (topLevelValue !== undefined)
500
+ assertDeepInterviewInputWithinLimit(topLevelValue as string, MAX_USER_RESPONSE_LENGTH, field);
501
+ }
502
+ if (!Array.isArray(state.rounds)) return;
503
+ for (const [index, round] of state.rounds.entries()) {
504
+ if (typeof round !== "object" || round === null || Array.isArray(round)) continue;
505
+ const record = round as Record<string, unknown>;
506
+ for (const field of ["custom_input", "customInput", "user_response", "answer"] as const) {
507
+ const value = record[field];
508
+ // A structured scorer may use an unrelated object-valued `answer`; only prose
509
+ // values in a legacy answer slot are user input subject to this cap.
510
+ if (value === undefined || (field === "answer" && typeof value !== "string")) continue;
511
+ assertDeepInterviewInputWithinLimit(
512
+ value as string,
513
+ MAX_USER_RESPONSE_LENGTH,
514
+ `state.rounds[${index}].${field}`,
515
+ );
516
+ }
517
+ }
518
+ }
519
+
400
520
  export interface DeepInterviewIntentItem {
401
521
  id: string;
402
522
  category: DeepInterviewIntentCategory;
@@ -437,7 +557,7 @@ export function isCanonicalDeepInterviewAnswerHash(value: unknown): value is str
437
557
  function isEvidenceReference(value: unknown, answerHashValue: string): value is string {
438
558
  return (
439
559
  typeof value === "string" &&
440
- value.length <= MAX_INTENT_EVIDENCE_LENGTH &&
560
+ deepInterviewCharacterCount(value) <= MAX_INTENT_EVIDENCE_LENGTH &&
441
561
  value === `answer_hash:${answerHashValue}`
442
562
  );
443
563
  }
@@ -451,7 +571,11 @@ function assertIntentItem(value: unknown): asserts value is DeepInterviewIntentI
451
571
  !id.startsWith(`${category}:`)
452
572
  )
453
573
  throw new Error("invalid intent category");
454
- if (typeof statement !== "string" || !statement.trim() || statement.length > MAX_INTENT_STATEMENT_LENGTH)
574
+ if (
575
+ typeof statement !== "string" ||
576
+ !statement.trim() ||
577
+ deepInterviewCharacterCount(statement) > MAX_INTENT_STATEMENT_LENGTH
578
+ )
455
579
  throw new Error(`intent item ${id} requires a bounded statement`);
456
580
  }
457
581
 
@@ -541,7 +665,7 @@ export function reviewDeepInterviewIntent(
541
665
  substitution.replacement_ids.some(id => typeof id !== "string" || !observedIds.has(id)) ||
542
666
  typeof substitution.rationale !== "string" ||
543
667
  !substitution.rationale.trim() ||
544
- substitution.rationale.length > MAX_INTENT_RATIONALE_LENGTH
668
+ deepInterviewCharacterCount(substitution.rationale) > MAX_INTENT_RATIONALE_LENGTH
545
669
  )
546
670
  throw new Error("invalid intent substitution");
547
671
  }
@@ -1,4 +1,3 @@
1
- import { Buffer } from "node:buffer";
2
1
  import * as crypto from "node:crypto";
3
2
  import * as fs from "node:fs";
4
3
  import * as path from "node:path";
@@ -6,6 +5,18 @@ import { VERSION } from "@gajae-code/utils/dirs";
6
5
  import { safeStderrWrite } from "@gajae-code/utils/safe-stderr";
7
6
  import type { Args } from "../cli/args";
8
7
  import { readLinuxProcStartTimeSync } from "./linux-proc";
8
+ import {
9
+ MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
10
+ MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
11
+ MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
12
+ MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
13
+ MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
14
+ } from "./managed-owner-admission";
15
+ import {
16
+ MANAGED_OWNER_INCARNATION_ENV,
17
+ MANAGED_OWNER_RUN_ID_ENV,
18
+ MANAGED_OWNER_SUPERVISOR_ARG,
19
+ } from "./managed-owner-supervisor";
9
20
  import { tmuxRuntimeSessionPath } from "./session-layout";
10
21
  import {
11
22
  GJC_COORDINATOR_SESSION_BRANCH_ENV,
@@ -42,6 +53,7 @@ import {
42
53
  type OwnerIsolationProbeSync,
43
54
  planTmuxOwnerIsolationSync,
44
55
  replaceOwnerGenerationSync,
56
+ resolveManagedOwnerPredecessorSync,
45
57
  type TmuxServerProof,
46
58
  } from "./tmux-owner-isolation";
47
59
  import {
@@ -152,6 +164,9 @@ export interface TmuxLaunchPlan {
152
164
  sessionStateFile?: string | null;
153
165
  /** Immutable Linux-managed owner provenance, assigned immediately before creation. */
154
166
  ownerGeneration?: string;
167
+ /** Immutable run and endpoint identities bound into the supervised command. */
168
+ ownerRunId?: string;
169
+ ownerIncarnation?: string;
155
170
  /** Generation state captured before owner-isolation planning; required for publication CAS. */
156
171
  ownerGenerationBaseline?: import("./tmux-owner-isolation").OwnerGenerationBaseline;
157
172
  /** Native tmux session identity emitted atomically by `new-session -P -F`. */
@@ -334,6 +349,7 @@ interface CommandResolutionContext {
334
349
  extraEnv?: Record<string, string>;
335
350
  tmuxExitMarkerPath?: string;
336
351
  platform?: NodeJS.Platform;
352
+ managedOwnerSupervisor?: boolean;
337
353
  }
338
354
 
339
355
  function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
@@ -553,8 +569,15 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
553
569
  tmuxExitMarkerPath: context.tmuxExitMarkerPath,
554
570
  });
555
571
  const command = resolveCurrentGjcCommand(context);
556
- const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
557
- const invocation = `env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
572
+ const childArgs = stripRootTmuxFlag(rawArgs);
573
+ const supervisorEnv: Record<string, string> = context.managedOwnerSupervisor
574
+ ? { GJC_MANAGED_OWNER_COMMAND_JSON: JSON.stringify([...command, ...childArgs]) }
575
+ : {};
576
+ const invocationArgs = context.managedOwnerSupervisor
577
+ ? [...command, MANAGED_OWNER_SUPERVISOR_ARG]
578
+ : [...command, ...childArgs];
579
+ const quoted = invocationArgs.map(shellQuote).join(" ");
580
+ const invocation = `env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments({ ...context.extraEnv, ...supervisorEnv })} ${quoted}`;
558
581
  if (!context.tmuxExitMarkerPath) return `exec ${invocation}`;
559
582
  return `${buildPosixTmuxExitMarkerPrefix(context.tmuxExitMarkerPath)}; ${invocation}; exit $?`;
560
583
  }
@@ -1026,15 +1049,29 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
1026
1049
  };
1027
1050
  }
1028
1051
 
1052
+ function trustedReplacementAuthority(
1053
+ stateDir: string,
1054
+ sessionId: string,
1055
+ baseline: ReturnType<typeof captureOwnerGenerationBaselineSync>,
1056
+ ): ReturnType<typeof resolveManagedOwnerPredecessorSync> {
1057
+ return resolveManagedOwnerPredecessorSync(stateDir, sessionId, baseline);
1058
+ }
1059
+
1029
1060
  function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchContext): void {
1030
1061
  if (plan.ownerGeneration) return;
1031
1062
  const sessionId = plan.sessionId ?? plan.sessionName;
1032
1063
  const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
1064
+ const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
1065
+ const replacement = trustedReplacementAuthority(stateDir, sessionId, baseline);
1033
1066
  const generation = crypto.randomUUID();
1034
- // Stage the generation in the child command. It becomes current only after
1067
+ const runId = crypto.randomUUID();
1068
+ const incarnation = crypto.randomUUID();
1069
+ plan.ownerGenerationBaseline = baseline;
1070
+ // Stage immutable identity in the child command. It becomes current only after
1035
1071
  // immutable creation proof and ownership tagging complete.
1036
-
1037
1072
  plan.ownerGeneration = generation;
1073
+ plan.ownerRunId = runId;
1074
+ plan.ownerIncarnation = incarnation;
1038
1075
  const innerCommand = buildInnerCommand(
1039
1076
  {
1040
1077
  cwd: plan.cwd,
@@ -1047,11 +1084,24 @@ function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchC
1047
1084
  [GJC_TMUX_OWNER_GENERATION_ENV]: generation,
1048
1085
  [GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
1049
1086
  [GJC_TMUX_OWNER_SERVER_KEY_ENV]: plan.tmuxCommand,
1087
+ [MANAGED_OWNER_RUN_ID_ENV]: runId,
1088
+ [MANAGED_OWNER_INCARNATION_ENV]: incarnation,
1089
+ ...(replacement
1090
+ ? {
1091
+ [MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: replacement.predecessorToken,
1092
+ [MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: replacement.generation,
1093
+ [MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: replacement.runId,
1094
+ [MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: replacement.incarnation,
1095
+ [MANAGED_OWNER_TRANSCRIPT_PATH_ENV]:
1096
+ context.env?.GJC_SESSION_FILE ?? process.env.GJC_SESSION_FILE ?? "",
1097
+ }
1098
+ : {}),
1050
1099
  },
1051
1100
  // Linux managed owner close signals the pane PID. Do not place the exit-marker shell
1052
1101
  // in front of it; `buildInnerCommand` therefore execs the GJC owner directly.
1053
1102
  tmuxExitMarkerPath: plan.platform === "linux" ? undefined : tmuxExitMarkerPath(plan.sessionStateFile ?? ""),
1054
1103
  platform: plan.platform,
1104
+ managedOwnerSupervisor: plan.platform === "linux",
1055
1105
  },
1056
1106
  context.rawArgs,
1057
1107
  );
@@ -1182,7 +1232,7 @@ function createIsolatedTmuxSession(
1182
1232
  ): TmuxSpawnResult {
1183
1233
  const sessionId = plan.sessionId ?? plan.sessionName;
1184
1234
  const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
1185
- const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
1235
+ const baseline = plan.ownerGenerationBaseline ?? captureOwnerGenerationBaselineSync(stateDir, sessionId);
1186
1236
  plan.ownerGenerationBaseline = baseline;
1187
1237
  const ownerPlan = planTmuxOwnerIsolationSync(
1188
1238
  {
@@ -1572,6 +1622,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
1572
1622
  return true;
1573
1623
  }
1574
1624
  try {
1625
+ resolveManagedOwnerPredecessorSync(
1626
+ path.dirname(plan.sessionStateFile!),
1627
+ plan.sessionId!,
1628
+ plan.ownerGenerationBaseline!,
1629
+ );
1575
1630
  replaceOwnerGenerationSync(
1576
1631
  path.dirname(plan.sessionStateFile!),
1577
1632
  plan.sessionId!,