sneakoscope 8.7.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/config/skills-manifest.json +1 -1
  5. package/dist/core/agents/agent-janitor.js +7 -0
  6. package/dist/core/agents/agent-orchestrator.js +2 -2
  7. package/dist/core/agents/agent-recursion-guard.js +18 -4
  8. package/dist/core/agents/native-cli-worker-runtime.js +83 -56
  9. package/dist/core/align/align-context-index.js +68 -0
  10. package/dist/core/align/align-route.js +2 -0
  11. package/dist/core/align/code-navigation-align.js +42 -42
  12. package/dist/core/codex-lb/desktop-bridge/http-forward.js +76 -7
  13. package/dist/core/codex-lb/desktop-bridge/server.js +2 -1
  14. package/dist/core/commands/search-command.js +4 -1
  15. package/dist/core/commands/triwiki-atlas-command.js +4 -4
  16. package/dist/core/commands/triwiki-graph-command.js +2 -2
  17. package/dist/core/commands/wiki-command.js +2 -2
  18. package/dist/core/fsx.js +1 -1
  19. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +2 -2
  20. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +8 -1
  21. package/dist/core/init.js +4 -0
  22. package/dist/core/naruto/context-graph-advisor-pairs.js +29 -0
  23. package/dist/core/naruto/context-graph-advisor-scope.js +77 -139
  24. package/dist/core/naruto/context-graph-advisor.js +0 -0
  25. package/dist/core/release/gate-affected-globs.js +93 -0
  26. package/dist/core/release/gate-manifest.js +2 -88
  27. package/dist/core/runtime/task-profile.js +3 -0
  28. package/dist/core/runtime/verification-budget.js +25 -0
  29. package/dist/core/search/context-projection.js +130 -0
  30. package/dist/core/search/context.js +0 -0
  31. package/dist/core/subagents/official-subagent-preparation.js +12 -3
  32. package/dist/core/subagents/official-subagent-runner.js +7 -0
  33. package/dist/core/subagents/triwiki-attention.js +2 -1
  34. package/dist/core/triwiki/context-graph/compiler/cache-key.js +11 -2
  35. package/dist/core/triwiki/context-graph/compiler/fragment-manifest-schema.js +163 -0
  36. package/dist/core/triwiki/context-graph/compiler/fragment-manifest-store.js +41 -0
  37. package/dist/core/triwiki/context-graph/compiler/fragment-manifest.js +108 -0
  38. package/dist/core/triwiki/context-graph/compiler/fragment-merge.js +195 -0
  39. package/dist/core/triwiki/context-graph/compiler/fragment-plan.js +137 -0
  40. package/dist/core/triwiki/context-graph/compiler/fragment-store.js +82 -0
  41. package/dist/core/triwiki/context-graph/compiler/incremental-build.js +161 -0
  42. package/dist/core/triwiki/context-graph/compiler/incremental-extract.js +94 -0
  43. package/dist/core/triwiki/context-graph/compiler/k-way-merge.js +92 -0
  44. package/dist/core/triwiki/context-graph/compiler/publish-index.js +98 -0
  45. package/dist/core/triwiki/context-graph/compiler/source-fragment.js +74 -0
  46. package/dist/core/triwiki/context-graph/contracts.js +2 -0
  47. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +4 -1
  48. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +53 -0
  49. package/dist/core/triwiki/context-graph/projections/anchors.js +23 -21
  50. package/dist/core/triwiki/context-graph/projections/attention.js +30 -36
  51. package/dist/core/triwiki/context-graph/projections/code-pack-entry.js +92 -0
  52. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +35 -44
  53. package/dist/core/triwiki/context-graph/projections/code-pack.js +21 -101
  54. package/dist/core/triwiki/context-graph/projections/graph-facts.js +103 -0
  55. package/dist/core/triwiki/context-graph/projections/index.js +3 -0
  56. package/dist/core/triwiki/context-graph/projections/module-view.js +46 -89
  57. package/dist/core/triwiki/context-graph/projections/node-summary.js +18 -32
  58. package/dist/core/triwiki/context-graph/projections/projection-candidate.js +47 -0
  59. package/dist/core/triwiki/context-graph/query/cache.js +191 -0
  60. package/dist/core/triwiki/context-graph/query/changed-path-seeds.js +29 -0
  61. package/dist/core/triwiki/context-graph/query/hydrate-chain.js +105 -0
  62. package/dist/core/triwiki/context-graph/query/hydrate-verify.js +83 -0
  63. package/dist/core/triwiki/context-graph/query/hydrate.js +0 -0
  64. package/dist/core/triwiki/context-graph/query/index.js +6 -0
  65. package/dist/core/triwiki/context-graph/query/kernel-candidates.js +128 -0
  66. package/dist/core/triwiki/context-graph/query/kernel-frontier.js +141 -0
  67. package/dist/core/triwiki/context-graph/query/kernel-fuse.js +240 -0
  68. package/dist/core/triwiki/context-graph/query/kernel-lanes.js +149 -0
  69. package/dist/core/triwiki/context-graph/query/kernel-plan.js +178 -0
  70. package/dist/core/triwiki/context-graph/query/kernel-safety.js +94 -0
  71. package/dist/core/triwiki/context-graph/query/kernel-select.js +154 -0
  72. package/dist/core/triwiki/context-graph/query/kernel-traverse.js +194 -0
  73. package/dist/core/triwiki/context-graph/query/kernel-types.js +63 -0
  74. package/dist/core/triwiki/context-graph/query/kernel.js +69 -0
  75. package/dist/core/triwiki/context-graph/query/name-anchors.js +20 -0
  76. package/dist/core/triwiki/context-graph/query/ranking-config.js +56 -0
  77. package/dist/core/triwiki/context-graph/query/walk.js +136 -0
  78. package/dist/core/triwiki/context-graph/query/workspace.js +110 -0
  79. package/dist/core/triwiki/context-graph/runtime-index/format-contract.js +111 -0
  80. package/dist/core/triwiki/context-graph/runtime-index/format-header.js +148 -0
  81. package/dist/core/triwiki/context-graph/runtime-index/format-primitives.js +57 -0
  82. package/dist/core/triwiki/context-graph/runtime-index/format-sections.js +57 -0
  83. package/dist/core/triwiki/context-graph/runtime-index/format.js +4 -0
  84. package/dist/core/triwiki/context-graph/runtime-index/lexicon-bm25.js +80 -0
  85. package/dist/core/triwiki/context-graph/runtime-index/lexicon-builder.js +194 -0
  86. package/dist/core/triwiki/context-graph/runtime-index/lexicon-contract.js +72 -0
  87. package/dist/core/triwiki/context-graph/runtime-index/lexicon-text.js +124 -0
  88. package/dist/core/triwiki/context-graph/runtime-index/lexicon-tokenizer.js +237 -0
  89. package/dist/core/triwiki/context-graph/runtime-index/lexicon.js +5 -0
  90. package/dist/core/triwiki/context-graph/runtime-index/reader-cursor.js +132 -0
  91. package/dist/core/triwiki/context-graph/runtime-index/reader-errors.js +39 -0
  92. package/dist/core/triwiki/context-graph/runtime-index/reader-hydrate.js +129 -0
  93. package/dist/core/triwiki/context-graph/runtime-index/reader-layout.js +104 -0
  94. package/dist/core/triwiki/context-graph/runtime-index/reader-lookup.js +90 -0
  95. package/dist/core/triwiki/context-graph/runtime-index/reader-names.js +57 -0
  96. package/dist/core/triwiki/context-graph/runtime-index/reader-types.js +2 -0
  97. package/dist/core/triwiki/context-graph/runtime-index/reader-validate.js +198 -0
  98. package/dist/core/triwiki/context-graph/runtime-index/reader.js +146 -0
  99. package/dist/core/triwiki/context-graph/runtime-index/writer-assemble.js +49 -0
  100. package/dist/core/triwiki/context-graph/runtime-index/writer-contract.js +112 -0
  101. package/dist/core/triwiki/context-graph/runtime-index/writer-lexical.js +139 -0
  102. package/dist/core/triwiki/context-graph/runtime-index/writer-tables.js +158 -0
  103. package/dist/core/triwiki/context-graph/runtime-index/writer-types.js +1 -0
  104. package/dist/core/triwiki/context-graph/runtime-index/writer.js +217 -0
  105. package/dist/core/triwiki/context-graph/store/generation-commit.js +171 -0
  106. package/dist/core/triwiki/context-graph/store/generation-errors.js +60 -0
  107. package/dist/core/triwiki/context-graph/store/generation-io.js +45 -0
  108. package/dist/core/triwiki/context-graph/store/generation-layout.js +42 -0
  109. package/dist/core/triwiki/context-graph/store/generation-pointer.js +131 -0
  110. package/dist/core/triwiki/context-graph/store/generation-recovery.js +89 -0
  111. package/dist/core/triwiki/context-graph/store/generation-resolve.js +24 -0
  112. package/dist/core/triwiki/context-graph/store/generation-retention.js +57 -0
  113. package/dist/core/triwiki/context-graph/store/generation-store.js +9 -0
  114. package/dist/core/triwiki/context-graph/store/generation-verify.js +48 -0
  115. package/dist/core/triwiki/context-graph/store/index-freshness.js +107 -0
  116. package/dist/core/triwiki/context-graph/store/operation-journal-recovery.js +37 -0
  117. package/dist/core/triwiki/context-graph/store/operation-journal-schema.js +225 -0
  118. package/dist/core/triwiki/context-graph/store/operation-journal.js +32 -0
  119. package/dist/core/triwiki/context-graph/store/snapshot-store.js +16 -8
  120. package/dist/core/verification/context-graph-affected.js +139 -98
  121. package/dist/core/verification/machine-feedback.js +19 -3
  122. package/dist/core/verification/verification-worker-pool.js +3 -1
  123. package/dist/core/version.js +1 -1
  124. package/dist/scripts/architecture-map-check.js +1 -1
  125. package/dist/scripts/context-graph-v2-check.js +327 -0
  126. package/package.json +2 -2
  127. package/release-gates.v2.json +128 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
22
22
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
23
23
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
24
24
 
25
- This README documents package **SKS 8.7.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
25
+ This README documents package **SKS 9.0.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
26
26
 
27
27
  Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
28
28
 
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "8.7.0"
262
+ version = "9.0.0"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "8.7.0"
3
+ version = "9.0.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "8.7.0",
3
+ "package_version": "9.0.0",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { exists, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
4
4
  import { normalizeAgentSessionRows } from './agent-session-rows.js';
5
5
  import { resolveOwnedNamespacePath } from './agent-namespace-safety.js';
6
+ import { runAgentCleanupExecutor } from './agent-cleanup-executor.js';
6
7
  export async function runAgentJanitor(input) {
7
8
  const staleMs = input.staleMs ?? 30 * 60 * 1000;
8
9
  const agentRoot = path.join(input.missionDir, 'agents');
@@ -39,6 +40,11 @@ export async function runAgentJanitor(input) {
39
40
  const orphanTempDirs = await scopedExistingPaths(Array.isArray(namespace?.orphan_temp_dirs) ? namespace.orphan_temp_dirs : [], projectHash, namespace?.temp_dir ? [namespace.temp_dir] : []);
40
41
  const staleLocks = await scopedStaleLockPaths(namespace?.lock_dir ? [namespace.lock_dir] : [], projectHash, staleMs);
41
42
  const cleaned = [];
43
+ const reaped = input.reapOrphanProcesses
44
+ ? await runAgentCleanupExecutor({ missionDir: input.missionDir, missionId: input.missionId ?? null, action: 'cleanup', apply: true, staleMs })
45
+ .then((report) => (report?.actions || []).filter((row) => row?.kind === 'terminate_process' && row?.status === 'applied').length)
46
+ .catch(() => 0)
47
+ : 0;
42
48
  if (input.cleanup) {
43
49
  for (const dir of orphanGenerationDirs) {
44
50
  await fsp.rm(path.join(agentRoot, dir), { recursive: true, force: true }).catch(() => { });
@@ -69,6 +75,7 @@ export async function runAgentJanitor(input) {
69
75
  slot_generation_cleanup: cleaned.filter((entry) => entry.includes(`${path.sep}sessions${path.sep}`)),
70
76
  orphan_temp_dirs: orphanTempDirs,
71
77
  stale_locks: staleLocks,
78
+ reaped_orphan_processes: reaped,
72
79
  cleaned,
73
80
  blockers,
74
81
  };
@@ -216,7 +216,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
216
216
  partition = applyNarutoWorkGraphToPartition(partition, opts.narutoWorkGraph, roster, targetActiveSlots, prompt);
217
217
  augmentVerificationRollbackDagForNaruto(strategyCompiled.verification_rollback_dag, partition.slices);
218
218
  }
219
- await runAgentJanitor({ missionDir: dir, missionId, projectHash: namespace.root_hash });
219
+ const preSpawnSweep = await runAgentJanitor({ missionDir: dir, missionId, projectHash: namespace.root_hash, reapOrphanProcesses: true });
220
220
  const ledgerRoot = await initializeAgentCentralLedger(dir, { missionId, roster, partition, route, prompt, dynamicScheduler: true });
221
221
  const triwikiContext = await loadTriWikiRuntimeContext(root);
222
222
  await writeTriWikiContextArtifact(ledgerRoot, triwikiContext);
@@ -617,7 +617,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
617
617
  backend,
618
618
  ledger_root: path.relative(root, ledgerRoot),
619
619
  roster,
620
- partition: { ok: partition.ok, slice_count: partition.slices.length, lease_count: partition.leases.length, blockers: partition.blockers },
620
+ partition: { ok: partition.ok, slice_count: partition.slices.length, lease_count: partition.leases.length, blockers: partition.blockers, pre_spawn_reaped: preSpawnSweep.reaped_orphan_processes },
621
621
  task_graph: partition.task_graph?.route_work_count_summary || null,
622
622
  requested_work_items: partition.task_graph?.desired_work_items || desiredWorkItemCount,
623
623
  actual_total_work_items: partition.task_graph?.total_work_items || partition.slices.length,
@@ -86,8 +86,9 @@ export function agentWorkerHookRecursionDecision(state = {}, payload = {}, comma
86
86
  reason: `Agent command recursion guard blocked nested SKS route command in Codex PreToolUse hook: ${guard.violations.join(', ')}`
87
87
  };
88
88
  }
89
- export function agentWorkerHookContext(state = {}, payload = {}) {
90
- const env = {
89
+ export function agentWorkerHookContext(state = {}, payload = {}, env = process.env) {
90
+ const declared = {
91
+ ...(env || {}),
91
92
  ...(payload.env || {}),
92
93
  ...(payload.tool_input?.env || {}),
93
94
  ...(payload.toolInput?.env || {}),
@@ -95,11 +96,24 @@ export function agentWorkerHookContext(state = {}, payload = {}) {
95
96
  ...(payload.tool?.input?.env || {})
96
97
  };
97
98
  void state;
98
- return Boolean(String(env.SKS_AGENT_WORKER || '') === '1'
99
- || String(env.SKS_DISABLE_ROUTE_RECURSION || '') === '1'
99
+ return Boolean(String(declared.SKS_AGENT_WORKER || '') === '1'
100
+ || String(declared.SKS_DISABLE_ROUTE_RECURSION || '') === '1'
101
+ || agentGenerationDepth(declared) > 0
100
102
  || payload.agent_worker === true
101
103
  || payload.agentWorker === true);
102
104
  }
105
+ export function agentGenerationDepth(env = process.env) {
106
+ const raw = Number.parseInt(String(env[AGENT_GENERATION_DEPTH_ENV] || ''), 10);
107
+ return Number.isFinite(raw) && raw > 0 ? Math.min(raw, MAX_AGENT_GENERATION_DEPTH * 4) : 0;
108
+ }
109
+ export const AGENT_GENERATION_DEPTH_ENV = 'SKS_AGENT_GENERATION_DEPTH';
110
+ export const MAX_AGENT_GENERATION_DEPTH = 1;
111
+ export function nextAgentGenerationEnv(env = process.env) {
112
+ return { [AGENT_GENERATION_DEPTH_ENV]: String(agentGenerationDepth(env) + 1) };
113
+ }
114
+ export function agentGenerationDepthExceeded(env = process.env) {
115
+ return agentGenerationDepth(env) > MAX_AGENT_GENERATION_DEPTH;
116
+ }
103
117
  export function assertNoAgentRecursion(text) {
104
118
  const result = scanAgentTextForRecursion(text);
105
119
  if (!result.ok)
@@ -1,13 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import { spawn } from 'node:child_process';
3
3
  import path from 'node:path';
4
- import { ensureDir, exists, nowIso, packageRoot, readJson, writeJsonAtomic } from '../fsx.js';
4
+ import { ensureDir, exists, nowIso, packageRoot, readJson, registerDetachedProcessGroup, terminateProcessTree, writeJsonAtomic } from '../fsx.js';
5
5
  import { fastModeEnv } from './fast-mode-policy.js';
6
6
  import { validateAgentWorkerResult } from './agent-worker-pipeline.js';
7
7
  import { appendParallelRuntimeEvent } from './parallel-runtime-proof.js';
8
8
  import { appendAgentMessage } from './agent-message-bus.js';
9
9
  import { markLoopWorkerInterrupted, registerLoopActiveWorker } from '../loops/loop-interrupt-registry.js';
10
10
  export const NATIVE_CLI_WORKER_RUNTIME_SCHEMA = 'sks.native-cli-worker-runtime.v3';
11
+ export const NATIVE_CLI_WORKER_DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
12
+ export const NATIVE_CLI_WORKER_TIMEOUT_BLOCKER = 'native_cli_worker_timeout';
11
13
  export function createNativeCliWorkerRuntimeRecorder(root, input) {
12
14
  return new NativeCliWorkerRuntimeRecorder(root, input);
13
15
  }
@@ -63,7 +65,7 @@ class NativeCliWorkerRuntimeRecorder {
63
65
  recursion_guard_env: true
64
66
  };
65
67
  await writeJsonAtomic(path.join(this.root, intakeRel), intake);
66
- const workerEntrypoint = await resolveWorkerEntrypointPath();
68
+ const workerEntrypoint = this.input.workerEntrypointPath || await resolveWorkerEntrypointPath();
67
69
  const args = [workerEntrypoint, '--intake', path.join(this.root, intakeRel), '--json'];
68
70
  const record = {
69
71
  schema: 'sks.native-cli-worker-session-record.v2',
@@ -117,9 +119,16 @@ class NativeCliWorkerRuntimeRecorder {
117
119
  SKS_AGENT_SLOT_ID: String(ctx.agent.slot_id || ''),
118
120
  SKS_AGENT_GENERATION_INDEX: String(ctx.agent.generation_index || 1)
119
121
  },
120
- stdio: ['ignore', 'pipe', 'pipe']
122
+ stdio: ['ignore', 'pipe', 'pipe'],
123
+ detached: process.platform !== 'win32'
124
+ });
125
+ child.stdout?.pipe(stdout);
126
+ child.stderr?.pipe(stderr);
127
+ const unregisterProcessGroup = registerDetachedProcessGroup(child);
128
+ const supervisor = superviseWorkerChild(child, {
129
+ timeoutMs: resolveWorkerTimeoutMs(ctx.opts),
130
+ ...(ctx.opts?.signal ? { signal: ctx.opts.signal } : {})
121
131
  });
122
- const removeAbortListener = terminateChildOnAbort(child, ctx.opts?.signal);
123
132
  const exitPromise = new Promise((resolve) => {
124
133
  child.once('close', (code, signal) => resolve({ code, signal }));
125
134
  child.once('error', () => resolve({ code: 1, signal: null }));
@@ -130,48 +139,57 @@ class NativeCliWorkerRuntimeRecorder {
130
139
  if (child.pid)
131
140
  this.active.add(child.pid);
132
141
  this.maxObserved = Math.max(this.maxObserved, this.active.size);
133
- await this.record(record);
134
- const loopHandle = await registerLoopWorkerHandle({
135
- root: ctx.opts.projectRoot || this.input.projectRoot || ctx.opts.cwd || packageRoot(),
136
- env: ctx.opts.env || {},
137
- agentId: String(ctx.agent.id || ctx.agent.session_id || 'agent'),
138
- sessionId: ctx.agent.session_id || null,
139
- pid: child.pid || null
140
- });
141
- await appendParallelRuntimeEvent(this.root, this.input.missionId, {
142
- event_type: 'worker_process_spawned',
143
- slot_id: ctx.agent.slot_id || ctx.agent.id || null,
144
- generation_index: ctx.agent.generation_index || null,
145
- session_id: ctx.agent.session_id || null,
146
- pid: child.pid || null,
147
- backend: this.input.backend,
148
- placement: 'process',
149
- worktree_id: worktree?.id || null
150
- }).catch(() => undefined);
151
- await this.lifecycle(ctx, {
152
- eventType: 'worker_spawned',
153
- status: 'launching',
154
- artifacts: [intakeRel, heartbeatRel, resultRel, stdoutRel, stderrRel],
155
- logTail: `pid=${child.pid || 'unknown'}`
156
- });
157
- child.stdout?.pipe(stdout);
158
- child.stderr?.pipe(stderr);
159
- const exit = await exitPromise;
160
- removeAbortListener();
161
- stdout.end();
162
- stderr.end();
163
- if (child.pid)
164
- this.active.delete(child.pid);
142
+ let exit;
143
+ let loopHandle = null;
144
+ try {
145
+ await this.record(record);
146
+ loopHandle = await registerLoopWorkerHandle({
147
+ root: ctx.opts.projectRoot || this.input.projectRoot || ctx.opts.cwd || packageRoot(),
148
+ env: ctx.opts.env || {},
149
+ agentId: String(ctx.agent.id || ctx.agent.session_id || 'agent'),
150
+ sessionId: ctx.agent.session_id || null,
151
+ pid: child.pid || null
152
+ });
153
+ await appendParallelRuntimeEvent(this.root, this.input.missionId, {
154
+ event_type: 'worker_process_spawned',
155
+ slot_id: ctx.agent.slot_id || ctx.agent.id || null,
156
+ generation_index: ctx.agent.generation_index || null,
157
+ session_id: ctx.agent.session_id || null,
158
+ pid: child.pid || null,
159
+ backend: this.input.backend,
160
+ placement: 'process',
161
+ worktree_id: worktree?.id || null
162
+ }).catch(() => undefined);
163
+ await this.lifecycle(ctx, {
164
+ eventType: 'worker_spawned',
165
+ status: 'launching',
166
+ artifacts: [intakeRel, heartbeatRel, resultRel, stdoutRel, stderrRel],
167
+ logTail: `pid=${child.pid || 'unknown'}`
168
+ });
169
+ exit = await exitPromise;
170
+ }
171
+ finally {
172
+ supervisor.dispose();
173
+ await supervisor.terminate();
174
+ unregisterProcessGroup();
175
+ stdout.end();
176
+ stderr.end();
177
+ if (child.pid)
178
+ this.active.delete(child.pid);
179
+ }
165
180
  record.closed_at = nowIso();
166
181
  record.exit_code = exit.code;
167
182
  record.signal = exit.signal;
168
183
  record.status = exit.code === 0 ? 'closed' : 'failed';
184
+ const timeoutBlockers = supervisor.timedOut ? [NATIVE_CLI_WORKER_TIMEOUT_BLOCKER] : [];
185
+ if (supervisor.timedOut)
186
+ record.status = 'failed';
169
187
  if (loopHandle) {
170
188
  await markLoopWorkerInterrupted(ctx.opts.projectRoot || this.input.projectRoot || ctx.opts.cwd || packageRoot(), loopHandle.mission_id, loopHandle.worker_id, record.status === 'closed' ? 'completed' : 'failed').catch(() => undefined);
171
189
  }
172
190
  const parsed = await readJson(path.join(this.root, resultRel), null).catch(() => null);
173
191
  if (!parsed) {
174
- record.blockers = ['native_cli_worker_result_missing'];
192
+ record.blockers = [...timeoutBlockers, 'native_cli_worker_result_missing'];
175
193
  await this.lifecycle(ctx, {
176
194
  eventType: 'worker_failed',
177
195
  status: 'failed',
@@ -199,6 +217,8 @@ class NativeCliWorkerRuntimeRecorder {
199
217
  }
200
218
  const result = validateAgentWorkerResult({
201
219
  ...parsed,
220
+ ...(timeoutBlockers.length ? { status: 'failed' } : {}),
221
+ blockers: [...timeoutBlockers, ...(Array.isArray(parsed.blockers) ? parsed.blockers : [])],
202
222
  artifacts: [...new Set([...(Array.isArray(parsed.artifacts) ? parsed.artifacts : []), stdoutRel, stderrRel])]
203
223
  });
204
224
  record.status = result.status === 'done' ? 'closed' : result.status;
@@ -293,30 +313,37 @@ class NativeCliWorkerRuntimeRecorder {
293
313
  };
294
314
  }
295
315
  }
296
- function terminateChildOnAbort(child, signal) {
297
- if (!signal)
298
- return () => undefined;
299
- let hardKillTimer = null;
316
+ function superviseWorkerChild(child, input) {
317
+ let timedOut = false;
318
+ let teardown = null;
300
319
  const terminate = () => {
301
320
  if (child.exitCode !== null || child.signalCode !== null)
302
- return;
303
- child.kill('SIGTERM');
304
- hardKillTimer = setTimeout(() => {
305
- if (child.exitCode === null && child.signalCode === null)
306
- child.kill('SIGKILL');
307
- }, 1500);
308
- hardKillTimer.unref?.();
321
+ return Promise.resolve();
322
+ teardown = teardown || terminateProcessTree(child.pid, child);
323
+ return teardown;
309
324
  };
310
- if (signal.aborted)
311
- terminate();
312
- else
313
- signal.addEventListener('abort', terminate, { once: true });
314
- return () => {
315
- signal.removeEventListener('abort', terminate);
316
- if (hardKillTimer)
317
- clearTimeout(hardKillTimer);
325
+ const onAbort = () => { void terminate(); };
326
+ const timer = setTimeout(() => { timedOut = true; void terminate(); }, input.timeoutMs);
327
+ timer.unref?.();
328
+ if (input.signal) {
329
+ if (input.signal.aborted)
330
+ void terminate();
331
+ else
332
+ input.signal.addEventListener('abort', onAbort, { once: true });
333
+ }
334
+ return {
335
+ get timedOut() { return timedOut; },
336
+ terminate,
337
+ dispose() {
338
+ clearTimeout(timer);
339
+ input.signal?.removeEventListener('abort', onAbort);
340
+ }
318
341
  };
319
342
  }
343
+ function resolveWorkerTimeoutMs(opts) {
344
+ const candidate = Number(opts?.workerTimeoutMs ?? opts?.timeoutMs);
345
+ return Number.isFinite(candidate) && candidate > 0 ? candidate : NATIVE_CLI_WORKER_DEFAULT_TIMEOUT_MS;
346
+ }
320
347
  async function resolveWorkerEntrypointPath() {
321
348
  const distEntrypoint = path.join(packageRoot(), 'dist', 'core', 'agents', 'native-cli-worker-entry.js');
322
349
  if (await exists(distEntrypoint))
@@ -0,0 +1,68 @@
1
+ import path from 'node:path';
2
+ import { readSourceHashes } from '../triwiki/context-graph/compiler/freshness.js';
3
+ import { computeSourceInventoryFingerprint } from '../triwiki/context-graph/compiler/fragment-manifest.js';
4
+ import { publishContextIndexGeneration } from '../triwiki/context-graph/compiler/publish-index.js';
5
+ import { openWorkspaceContextIndex } from '../triwiki/context-graph/query/index.js';
6
+ import { ContextIndexStoreError } from '../triwiki/context-graph/store/generation-errors.js';
7
+ import { ContextIndexWriterError } from '../triwiki/context-graph/runtime-index/writer.js';
8
+ import { ContextIndexFormatError } from '../triwiki/context-graph/runtime-index/format.js';
9
+ import { projectCodePackFromGraph } from '../triwiki/code-pack.js';
10
+ export const ALIGN_PENDING_ROOT_DIR = 'pending';
11
+ export const ALIGN_CONTEXT_INDEX_BLOCKER = 'code_navigation_context_index_blocked';
12
+ export function alignPendingRoot(stageRoot) {
13
+ return path.join(stageRoot, ALIGN_PENDING_ROOT_DIR);
14
+ }
15
+ export function alignPendingWiki(stageRoot) {
16
+ return path.join(alignPendingRoot(stageRoot), '.sneakoscope', 'wiki');
17
+ }
18
+ export function alignSourceFingerprint(inputHashes) {
19
+ return computeSourceInventoryFingerprint(new Map(Object.entries(inputHashes)));
20
+ }
21
+ function alignBlocker(error) {
22
+ if (error instanceof ContextIndexStoreError) {
23
+ return new Error(`${ALIGN_CONTEXT_INDEX_BLOCKER}:${error.publicCode}:${error.code}`);
24
+ }
25
+ if (error instanceof ContextIndexWriterError || error instanceof ContextIndexFormatError) {
26
+ return new Error(`${ALIGN_CONTEXT_INDEX_BLOCKER}:${error.publicCode}:${error.code}`);
27
+ }
28
+ return error instanceof Error ? error : new Error(String(error));
29
+ }
30
+ export async function publishAlignContextIndex(input) {
31
+ try {
32
+ return await publishContextIndexGeneration({
33
+ root: input.pendingRoot,
34
+ snapshot: input.snapshot,
35
+ sourceFingerprint: input.sourceFingerprint,
36
+ fragmentManifestHash: input.fragmentManifestHash ?? null,
37
+ ...(input.lintErrors === undefined ? {} : { lintErrors: input.lintErrors }),
38
+ ...(input.lintWarnings === undefined ? {} : { lintWarnings: input.lintWarnings })
39
+ });
40
+ }
41
+ catch (error) {
42
+ throw alignBlocker(error);
43
+ }
44
+ }
45
+ function citedPaths(pack) {
46
+ return [...new Set(pack.entries.flatMap((entry) => entry.citations.map((citation) => citation.path)))].sort();
47
+ }
48
+ export async function projectAlignCodePack(input) {
49
+ let reader;
50
+ try {
51
+ const handle = await openWorkspaceContextIndex(input.pendingRoot, {
52
+ cache: null,
53
+ expectedSourceFingerprint: input.sourceFingerprint
54
+ });
55
+ reader = handle.reader;
56
+ }
57
+ catch (error) {
58
+ throw alignBlocker(error);
59
+ }
60
+ const options = {
61
+ generatedAt: input.generatedAt,
62
+ gitHeadSha: input.gitHeadSha,
63
+ snapshotFreshness: 'fresh'
64
+ };
65
+ const first = projectCodePackFromGraph(input.root, reader, options);
66
+ const observed = await readSourceHashes(input.root, citedPaths(first.pack));
67
+ return projectCodePackFromGraph(input.root, reader, { ...options, observedSourceHashes: observed }).pack;
68
+ }
@@ -20,6 +20,8 @@ export const ALIGN_SOURCE_POLICY = Object.freeze({
20
20
  export const ALIGN_OUTPUT_ARTIFACTS = Object.freeze([
21
21
  '.sneakoscope/wiki/context-graph.json',
22
22
  '.sneakoscope/wiki/context-graph.meta.json',
23
+ '.sneakoscope/wiki/context-graph/current.json',
24
+ '.sneakoscope/wiki/context-graph/context-graph.meta.json',
23
25
  '.sneakoscope/wiki/code-navigation-manifest.json',
24
26
  '.sneakoscope/wiki/code-pack.json',
25
27
  '.sneakoscope/wiki/context-pack.json',
@@ -8,17 +8,17 @@ import { createRequestedScopeContract } from '../safety/requested-scope-contract
8
8
  import { projectTriwikiToAgentsMdTransactional } from '../triwiki/agents-md-projector.js';
9
9
  import { buildCodeNavigationContextPack, validateCodeNavigationContextPack } from '../triwiki/code-navigation-context-pack.js';
10
10
  import { CODE_NAVIGATION_FATAL_SKIP_REASONS, CODE_NAVIGATION_LIMITS } from '../triwiki/code-navigation-policy.js';
11
- import { isCodePackProjectionBoundToSnapshot, projectCodePackFromGraph, validateCodePack } from '../triwiki/code-pack.js';
11
+ import { isCodePackProjectionBoundToSnapshot, validateCodePack } from '../triwiki/code-pack.js';
12
12
  import { validateContextGraphSnapshot } from '../triwiki/context-graph/contracts.js';
13
13
  import { compileContextGraph } from '../triwiki/context-graph/compiler/index.js';
14
- import { readSourceHashes } from '../triwiki/context-graph/compiler/freshness.js';
15
14
  import { ARCHITECTURE_MAP_EXTRACTOR_IDS, architectureMapGraphExtractors } from '../triwiki/context-graph/extractors/index.js';
16
15
  import { computeContextGraphCacheKey } from '../triwiki/context-graph/compiler/cache-key.js';
17
16
  import { walkCodeInventory } from '../triwiki/context-graph/extractors/code/inventory.js';
18
17
  import { codeInventoryInputHashes } from '../triwiki/code-navigation-policy.js';
19
- import { clearContextGraphSnapshotCache } from '../triwiki/context-graph/query/snapshot-cache.js';
18
+ import { clearContextGraphSnapshotCache, clearWorkspaceContextIndex } from '../triwiki/context-graph/query/index.js';
20
19
  import { withTriWikiStateLock } from '../triwiki/triwiki-cleanup.js';
21
20
  import { publishArchitectureMapToStage } from '../triwiki/context-graph/store/architecture-map-store.js';
21
+ import { alignPendingRoot, alignPendingWiki, alignSourceFingerprint, projectAlignCodePack, publishAlignContextIndex } from './align-context-index.js';
22
22
  import { ALIGN_GATE_ARTIFACT, ALIGN_LEDGER_ARTIFACT, ALIGN_OUTPUT_ARTIFACTS, ALIGN_PLAN_ARTIFACT, ALIGN_STAGING_ROOT_REL, buildInitialAlignLedger, refreshAlignGate } from './align-route.js';
23
23
  const WIKI_PREFIX = '.sneakoscope/wiki/';
24
24
  function isArchitectureMapOptionalSkip(skip) {
@@ -65,9 +65,6 @@ function countsBy(values, key) {
65
65
  function difference(left, right) {
66
66
  return left.filter((value) => !right.has(value)).sort();
67
67
  }
68
- function citedPaths(pack) {
69
- return [...new Set(pack.entries.flatMap((entry) => entry.citations.map((citation) => citation.path)))].sort();
70
- }
71
68
  function alignGuard(root) {
72
69
  const contract = createRequestedScopeContract({
73
70
  route: '$sks-align',
@@ -127,20 +124,6 @@ async function writeEvidence(root, dir, ledger, missionId) {
127
124
  await writeJsonAtomic(path.join(dir, ALIGN_LEDGER_ARTIFACT), ledger);
128
125
  return (await refreshAlignGate(dir, missionId, root)).gate;
129
126
  }
130
- async function buildPack(root, snapshot, meta, generatedAt) {
131
- const first = projectCodePackFromGraph(root, snapshot, {
132
- generatedAt,
133
- gitHeadSha: meta.cacheKeyParts.head,
134
- snapshotFreshness: 'fresh'
135
- });
136
- const observed = await readSourceHashes(root, citedPaths(first.pack));
137
- return projectCodePackFromGraph(root, snapshot, {
138
- generatedAt,
139
- gitHeadSha: meta.cacheKeyParts.head,
140
- snapshotFreshness: 'fresh',
141
- observedSourceHashes: observed
142
- }).pack;
143
- }
144
127
  function buildManifest(generatedAt, inventory, inventoryDigest, snapshot) {
145
128
  return {
146
129
  schema: 'sks.code-navigation-manifest.v1',
@@ -276,29 +259,9 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
276
259
  if (!graphValidation.ok || meta.cacheKeyParts.sourcePolicy !== 'workspace') {
277
260
  throw new Error('code_navigation_graph_validation_failed');
278
261
  }
279
- const pack = await buildPack(root, snapshot, meta, generatedAt);
280
- const packValidation = await validateCodePack(pack, root);
281
- ledger.validation.code_pack = packValidation.ok
282
- && pack.source_file_count === inventoryPaths.length
283
- && isCodePackProjectionBoundToSnapshot(snapshot.snapshotHash, pack);
284
- if (!ledger.validation.code_pack)
285
- throw new Error(`code_navigation_code_pack_invalid:${packValidation.issues.join('|')}`);
286
- const contextPack = buildCodeNavigationContextPack({
287
- root,
288
- codePack: pack,
289
- snapshotHash: snapshot.snapshotHash,
290
- fileCount: inventoryPaths.length,
291
- symbolCount: ledger.graph.symbol_nodes,
292
- edgeCount: snapshot.edgeCount,
293
- extractorRevisions: snapshot.extractors.map(({ id, revision }) => ({ id, revision }))
294
- });
295
- const contextValidation = validateCodeNavigationContextPack(contextPack, root);
296
- ledger.validation.context_pack = contextValidation.ok;
297
- if (!contextValidation.ok)
298
- throw new Error('code_navigation_context_pack_invalid');
299
- const manifest = buildManifest(generatedAt, inventory, codeInventoryDigest, snapshot);
300
262
  const stageRoot = path.join(stageBase, `${missionId}-${randomUUID().slice(0, 8)}`);
301
- const stageWiki = path.join(stageRoot, 'new-wiki');
263
+ const pendingRoot = alignPendingRoot(stageRoot);
264
+ const stageWiki = alignPendingWiki(stageRoot);
302
265
  const previousRoot = path.join(stageRoot, 'previous');
303
266
  const activeWiki = path.join(root, '.sneakoscope', 'wiki');
304
267
  const movedPrior = [];
@@ -307,6 +270,41 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
307
270
  let commitStarted = false;
308
271
  try {
309
272
  await ensureDir(stageWiki);
273
+ const sourceFingerprint = alignSourceFingerprint(inputHashes);
274
+ const published = await publishAlignContextIndex({
275
+ pendingRoot,
276
+ snapshot,
277
+ sourceFingerprint,
278
+ fragmentManifestHash: null,
279
+ lintWarnings: compiled.issues.length
280
+ });
281
+ const pack = await projectAlignCodePack({
282
+ root,
283
+ pendingRoot,
284
+ gitHeadSha: meta.cacheKeyParts.head,
285
+ generatedAt,
286
+ sourceFingerprint: published.sourceFingerprint
287
+ });
288
+ const packValidation = await validateCodePack(pack, root);
289
+ ledger.validation.code_pack = packValidation.ok
290
+ && pack.source_file_count === inventoryPaths.length
291
+ && isCodePackProjectionBoundToSnapshot(snapshot.snapshotHash, pack);
292
+ if (!ledger.validation.code_pack)
293
+ throw new Error(`code_navigation_code_pack_invalid:${packValidation.issues.join('|')}`);
294
+ const contextPack = buildCodeNavigationContextPack({
295
+ root,
296
+ codePack: pack,
297
+ snapshotHash: snapshot.snapshotHash,
298
+ fileCount: inventoryPaths.length,
299
+ symbolCount: ledger.graph.symbol_nodes,
300
+ edgeCount: snapshot.edgeCount,
301
+ extractorRevisions: snapshot.extractors.map(({ id, revision }) => ({ id, revision }))
302
+ });
303
+ const contextValidation = validateCodeNavigationContextPack(contextPack, root);
304
+ ledger.validation.context_pack = contextValidation.ok;
305
+ if (!contextValidation.ok)
306
+ throw new Error('code_navigation_context_pack_invalid');
307
+ const manifest = buildManifest(generatedAt, inventory, codeInventoryDigest, snapshot);
310
308
  await writeJsonAtomic(path.join(stageWiki, 'context-graph.json'), snapshot);
311
309
  await writeJsonAtomic(path.join(stageWiki, 'context-graph.meta.json'), meta);
312
310
  await writeJsonAtomic(path.join(stageWiki, 'code-navigation-manifest.json'), manifest);
@@ -333,6 +331,7 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
333
331
  await guardedRename(guard, stageWiki, activeWiki);
334
332
  promoted = true;
335
333
  clearContextGraphSnapshotCache();
334
+ clearWorkspaceContextIndex(root);
336
335
  ledger.publication.transactional_directory_replaced = true;
337
336
  ledger.publication.active_artifacts = [...ALIGN_OUTPUT_ARTIFACTS];
338
337
  projection = await projectTriwikiToAgentsMdTransactional(root);
@@ -401,6 +400,7 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
401
400
  await ensureDir(path.dirname(stageWiki));
402
401
  await guardedRename(guard, activeWiki, stageWiki);
403
402
  clearContextGraphSnapshotCache();
403
+ clearWorkspaceContextIndex(root);
404
404
  }
405
405
  catch (rollbackError) {
406
406
  rollbackFailures.push(`wiki:${String(rollbackError)}`);