sneakoscope 4.8.5 → 4.8.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 (87) hide show
  1. package/README.md +3 -3
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/cli/command-registry.js +1 -0
  7. package/dist/core/agents/agent-central-ledger.js +10 -5
  8. package/dist/core/agents/agent-orchestrator.js +22 -1
  9. package/dist/core/agents/agent-runner-codex-exec.js +24 -0
  10. package/dist/core/agents/fast-mode-policy.js +3 -4
  11. package/dist/core/agents/native-cli-session-proof.js +1 -1
  12. package/dist/core/agents/native-cli-worker.js +73 -113
  13. package/dist/core/agents/worker-pane-communication-contract.js +2 -3
  14. package/dist/core/codex/codex-cli-syntax-builder.js +2 -0
  15. package/dist/core/codex-app/codex-init-deep.js +10 -3
  16. package/dist/core/codex-control/codex-reliability-shield.js +36 -17
  17. package/dist/core/codex-control/codex-sdk-adapter.js +2 -2
  18. package/dist/core/codex-control/codex-sdk-config-policy.js +4 -0
  19. package/dist/core/codex-control/codex-task-runner.js +23 -11
  20. package/dist/core/codex-native/core-skill-manifest.js +2 -1
  21. package/dist/core/commands/gates-command.js +58 -0
  22. package/dist/core/commands/naruto-command.js +1 -14
  23. package/dist/core/commands/pipeline-command.js +2 -2
  24. package/dist/core/commands/release-command.js +5 -5
  25. package/dist/core/commands/status-command.js +2 -3
  26. package/dist/core/commands/stop-gate-command.js +1 -1
  27. package/dist/core/fsx.js +1 -1
  28. package/dist/core/hooks-runtime/team-digest.js +86 -0
  29. package/dist/core/hooks-runtime.js +5 -90
  30. package/dist/core/init/skills.js +395 -0
  31. package/dist/core/init.js +2 -376
  32. package/dist/core/lean-engineering-policy.js +2 -1
  33. package/dist/core/mission.js +4 -41
  34. package/dist/core/naruto/naruto-active-pool.js +36 -7
  35. package/dist/core/naruto/naruto-real-worker-child.js +67 -11
  36. package/dist/core/permission-gates.js +45 -19
  37. package/dist/core/pipeline-internals/runtime-core.js +7 -10
  38. package/dist/core/pipeline-internals/runtime-gates.js +28 -62
  39. package/dist/core/ppt/artifacts.js +19 -0
  40. package/dist/core/ppt/html.js +96 -0
  41. package/dist/core/ppt/style-tokens.js +248 -0
  42. package/dist/core/ppt.js +6 -352
  43. package/dist/core/qa-loop/{qa-contract-v2.js → qa-contract.js} +1 -1
  44. package/dist/core/qa-loop/{qa-gate-v2.js → qa-gate.js} +1 -1
  45. package/dist/core/qa-loop/qa-runtime-artifacts.js +2 -2
  46. package/dist/core/qa-loop.js +11 -4
  47. package/dist/core/recallpulse/policy.js +226 -0
  48. package/dist/core/recallpulse.js +2 -225
  49. package/dist/core/release/release-gate-dag.js +6 -2
  50. package/dist/core/research/mock-result.js +406 -0
  51. package/dist/core/research/prompt.js +15 -0
  52. package/dist/core/research.js +2 -402
  53. package/dist/core/routes/constants.js +48 -0
  54. package/dist/core/routes/design-policy.js +37 -0
  55. package/dist/core/routes/evidence.js +77 -0
  56. package/dist/core/routes/ppt-policy.js +26 -0
  57. package/dist/core/routes.js +13 -190
  58. package/dist/core/safety/mutation-ledger.js +2 -0
  59. package/dist/core/safety/ssot-guard.js +2 -2
  60. package/dist/core/stop-gate/stop-gate-check.js +10 -17
  61. package/dist/core/stop-gate/stop-gate-resolver.js +0 -3
  62. package/dist/core/version.js +1 -1
  63. package/dist/core/zellij/zellij-slot-pane-renderer.js +15 -8
  64. package/dist/core/zellij/zellij-slot-telemetry.js +28 -12
  65. package/dist/scripts/check-pipeline-budget.js +1 -7
  66. package/dist/scripts/check-pipeline-runtime.js +1 -8
  67. package/dist/scripts/codex-control-all-pipelines-check.js +5 -14
  68. package/dist/scripts/codex-sdk-all-pipelines-check.js +5 -9
  69. package/dist/scripts/fast-codex-service-tier-proof-check.js +2 -2
  70. package/dist/scripts/gate-policy-audit-check.js +70 -0
  71. package/dist/scripts/official-docs-compat-report.js +2 -2
  72. package/dist/scripts/product-design-plugin-routing-check.js +10 -5
  73. package/dist/scripts/qa-loop-surface-router-check.js +2 -2
  74. package/dist/scripts/release-check-dynamic-execute.js +32 -35
  75. package/dist/scripts/release-check-dynamic.js +19 -28
  76. package/dist/scripts/release-check-stamp.js +2 -2
  77. package/dist/scripts/release-dag-full-coverage-check.js +57 -394
  78. package/dist/scripts/release-dynamic-performance-check.js +18 -19
  79. package/dist/scripts/release-dynamic-presets-check.js +2 -2
  80. package/dist/scripts/release-gate-dag-runner.js +3 -1
  81. package/dist/scripts/release-gate-existence-audit.js +59 -91
  82. package/dist/scripts/release-gate-planner.js +43 -23
  83. package/dist/scripts/release-metadata-1-19-check.js +100 -12
  84. package/dist/scripts/worker-pane-communication-contract-check.js +1 -2
  85. package/dist/scripts/zellij-slot-pane-renderer-check.js +3 -2
  86. package/package.json +13 -965
  87. package/dist/core/pipeline-runtime.js +0 -2
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  | 🍥 **`$Naruto` swarm** | Up to 100 parallel clone workers behind a hardware-safe concurrency governor, lease-based parallel writes, and a scheduler that backfills idle slots while runnable work remains |
25
25
  | 🖥️ **Live Zellij cockpit** | Workers stack vertically as native Zellij stacked panes; each pane streams heartbeat, current file, tool events, and stdout tails every second |
26
26
  | 🧾 **Completion Proof** | Every serious route ends with schema-backed proof artifacts — no "trust me, it's done" |
27
- | 🛡️ **Safety gates** | DB/migration/destructive operations stay gated, 190+ release gate scripts, Honest Mode |
27
+ | 🛡️ **Safety gates** | DB/migration/destructive operations stay gated, ≤200 release-preset gates, Honest Mode |
28
28
  | 🖼️ **Visual evidence** | Image Voxel TriWiki plus `$imagegen`/`gpt-image-2`-bound visual review flows |
29
29
 
30
30
  ## LLM Init Prompt for Codex Native Agent Builders
@@ -49,7 +49,7 @@ sks seo-geo-optimizer apply latest --mode seo --apply --json
49
49
  sks seo-geo-optimizer audit --mode geo --target package --offline --json
50
50
  ```
51
51
 
52
- > 📋 **Current release: `v4.8.5`** — full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
52
+ > 📋 **Current release: `v4.8.6`** — full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
53
53
 
54
54
  ## 🍥 Parallelism, UX, And Integrations
55
55
 
@@ -763,7 +763,7 @@ npm run release:check
763
763
  npm run publish:dry
764
764
  ```
765
765
 
766
- `release:check` runs the change-aware affected release gate for ordinary local checks. Publish readiness uses `release:check:full`, which runs the full release DAG and writes a source digest stamp under `.sneakoscope/reports/` so publish commands can verify the same source/dist state. The DAG preserves the 1.18 baseline gates and adds Codex 0.136 compatibility, inherited Codex 0.135/0.134 runner truth, patch swarm runtime truth, transaction journaling, serial conflict rebase, strict strategy-to-patch proof, rollback command proof, Native CLI Session Swarm 5/10/20-process proof, Real Worker Backend Router proof, Codex child overlap proof, model-authored patch-envelope separation, Zellij layout/pane/screen/socket-dir proof, no-subagent-scaling proof, Fast mode default/worker/Codex/MAD propagation proof, Appshots attachment provenance, MCP runtime overlap evidence, task graph expansion, schema-bound follow-up work, actual Agent/Team/Research/QA route blackboxes, scheduler proof hardening, Source Intelligence propagation, Goal mode propagation checks, slot telemetry, update notice, MAD-DB, and Naruto SSOT gates. Broader live gates remain explicit scripts such as `release:real-check`; real Codex patch smoke, real Codex parallel worker proof, and real Zellij proof are optional unless their `SKS_REQUIRE_REAL_*` or `SKS_REQUIRE_ZELLIJ=1` environment variables are set. Generate the human-readable registry with `sks features inventory --write-docs`. Plain `npm publish` uses the `latest` dist-tag. `npm run publish:dry` runs `release:check:full`, verifies the fresh stamp, and then performs provenance/registry and npm dry-run checks. `npm run publish:npm` and `npm run release:publish` run `npm run publish:prep-ignore-scripts` first, then `npm publish --ignore-scripts`, so the real publish path stays strict even when lifecycle scripts are skipped. Operators who intentionally run `npm publish --ignore-scripts` directly must run `npm run publish:prep-ignore-scripts` immediately before it. npm's `prepublishOnly` uses `prepublish-release-check-or-fast` to accept that current stamp before the real publish; if the stamp is missing or stale, it runs `release:check:full` once before continuing.
766
+ `release:check` runs the change-aware affected release gate for ordinary local checks. Publish readiness uses `release:check:full`, which runs the full release DAG and writes a source digest stamp under `.sneakoscope/reports/` so publish commands can verify the same source/dist state. The release preset is capped at 200 user-concern gates, with terminal harness gates split into `infra-harness-gates.json` and runnable through `sks gates run harness`. Gate commands execute directly from `release-gates.v2.json`; `package.json` keeps a small user-facing script surface. Broader live gates remain explicit commands; real Codex patch smoke, real Codex parallel worker proof, and real Zellij proof are optional unless their `SKS_REQUIRE_REAL_*` or `SKS_REQUIRE_ZELLIJ=1` environment variables are set.
767
767
 
768
768
  Version bumps are manual. Run `sks versioning bump` only when preparing release metadata; SKS will not create `.git/hooks/pre-commit` or auto-bump during ordinary commits.
769
769
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "4.8.5"
79
+ version = "4.8.6"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "4.8.5"
3
+ version = "4.8.6"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 4.8.5"),
7
+ Some("--version") => println!("sks-rs 4.8.6"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '4.8.5';
2
+ const FAST_PACKAGE_VERSION = '4.8.6';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -78,6 +78,7 @@ export const COMMANDS = {
78
78
  version: entry('stable', 'Show SKS version', 'dist/commands/version.js', directCommand(() => import('../commands/version.js'), 'dist/commands/version.js')),
79
79
  commands: entry('stable', 'List SKS commands', 'dist/core/commands/basic-cli.js', basicArgs('commandsCommand')),
80
80
  check: entry('stable', 'Run five-minute proof-bank affected checks', 'dist/core/commands/check-command.js', argsCommand(() => import('../core/commands/check-command.js'), 'checkCommand', 'dist/core/commands/check-command.js')),
81
+ gates: entry('stable', 'Run release gate DAG by gate id or preset', 'dist/core/commands/gates-command.js', argsCommand(() => import('../core/commands/gates-command.js'), 'gatesCommand', 'dist/core/commands/gates-command.js')),
81
82
  task: entry('stable', 'Run an SLA-bounded SKS task check', 'dist/core/commands/task-command.js', argsCommand(() => import('../core/commands/task-command.js'), 'taskCommand', 'dist/core/commands/task-command.js')),
82
83
  release: entry('stable', 'Run affected/full/background release gates', 'dist/core/commands/release-command.js', argsCommand(() => import('../core/commands/release-command.js'), 'releaseCommand', 'dist/core/commands/release-command.js')),
83
84
  triwiki: entry('stable', 'Inspect TriWiki index, affected graph, and proof bank', 'dist/core/commands/triwiki-command.js', argsCommand(() => import('../core/commands/triwiki-command.js'), 'triwikiCommand', 'dist/core/commands/triwiki-command.js')),
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { AGENT_LEDGER_EVENT_SCHEMA, AGENT_PROOF_EVIDENCE_SCHEMA } from './agent-schema.js';
5
- import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
5
+ import { appendJsonlBounded, ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
6
6
  import { redactSecrets } from '../secret-redaction.js';
7
7
  const LEDGER_LOCKS = new Map();
8
8
  export const AGENT_ORCHESTRATOR_ONLY_FILES = Object.freeze([
@@ -111,7 +111,7 @@ export async function appendAgentLedgerEvent(root, event) {
111
111
  };
112
112
  const current_hash = hashEntry(entryWithoutHash);
113
113
  const entry = { ...entryWithoutHash, current_hash };
114
- await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf8');
114
+ await appendJsonlBounded(file, entry, 5 * 1024 * 1024);
115
115
  return entry;
116
116
  });
117
117
  }
@@ -123,15 +123,20 @@ export async function validateAgentLedgerHashChain(root) {
123
123
  let expectedSequence = 1;
124
124
  for (const line of lines) {
125
125
  const entry = JSON.parse(line);
126
+ if (entry?.type === 'log.rotated') {
127
+ expectedSequence = 0;
128
+ previousHash = null;
129
+ continue;
130
+ }
126
131
  const { current_hash, ...withoutHash } = entry;
127
- if (entry.sequence !== expectedSequence)
132
+ if (expectedSequence > 0 && entry.sequence !== expectedSequence)
128
133
  blockers.push('sequence_mismatch:' + entry.sequence + ':' + expectedSequence);
129
- if ((entry.previous_hash || null) !== previousHash)
134
+ if (previousHash !== null && (entry.previous_hash || null) !== previousHash)
130
135
  blockers.push('previous_hash_mismatch:' + entry.sequence);
131
136
  if (hashEntry(withoutHash) !== current_hash)
132
137
  blockers.push('current_hash_mismatch:' + entry.sequence);
133
138
  previousHash = current_hash;
134
- expectedSequence += 1;
139
+ expectedSequence = Number(entry.sequence || expectedSequence) + 1;
135
140
  }
136
141
  return { ok: blockers.length === 0, entries: lines.length, blockers };
137
142
  }
@@ -66,6 +66,7 @@ import { applyGitWorktreeMergeQueue } from '../git/git-worktree-merge-queue.js';
66
66
  import { crossRebaseIdleWorktrees } from '../git/git-worktree-cross-rebase.js';
67
67
  import { gitOutputLine, runGitCommand } from '../git/git-worktree-runner.js';
68
68
  import { writeParallelRuntimeProof } from './parallel-runtime-proof.js';
69
+ import { enforceRetention } from '../retention.js';
69
70
  export async function runNativeAgentOrchestrator(opts = {}) {
70
71
  const root = path.resolve(opts.root || process.cwd());
71
72
  const prompt = String(opts.prompt || 'Native agent run');
@@ -345,6 +346,15 @@ export async function runNativeAgentOrchestrator(opts = {}) {
345
346
  zellijVisiblePaneCap: Number(opts.zellijVisiblePaneCap || visualLaneCount || targetActiveSlots),
346
347
  projectRoot: root
347
348
  });
349
+ const schedulerHardTimeoutMs = normalizeMissionHardTimeoutMs(opts, route);
350
+ let lastTimeoutReapMs = 0;
351
+ async function reapTimedOutAgentSessions(force = false) {
352
+ const now = Date.now();
353
+ if (!force && now - lastTimeoutReapMs < 30000)
354
+ return null;
355
+ lastTimeoutReapMs = now;
356
+ return killTimedOutAgentSessions(ledgerRoot, now, { hardTimeoutMs: schedulerHardTimeoutMs });
357
+ }
348
358
  await nativeCliSwarm.initialize();
349
359
  await setCurrent(root, { mission_id: missionId, mode: 'AGENT', phase: 'AGENT_NATIVE_KERNEL_RUNNING', route_command: routeCommand, native_agent_backend: backend });
350
360
  const scheduler = await runAgentScheduler({
@@ -361,6 +371,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
361
371
  sourceIntelligenceRefs: sourceIntelligenceRef,
362
372
  goalModeRef,
363
373
  launchSession: async ({ agent, workItem }) => {
374
+ await reapTimedOutAgentSessions();
364
375
  const slice = workItem.slice || { id: workItem.id, description: workItem.description || prompt };
365
376
  const workerWorktree = await prepareWorkerGitWorktree({
366
377
  root,
@@ -401,6 +412,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
401
412
  const result = opts.nativeCliSwarm === false
402
413
  ? await runAgentByBackend(backend, runtimeAgent, runtimeSlice, backendOpts)
403
414
  : await nativeCliSwarm.launchWorker({ agent: runtimeAgent, slice: runtimeSlice, opts: backendOpts });
415
+ await reapTimedOutAgentSessions();
404
416
  if (route === '$Naruto')
405
417
  attachNarutoRuntimeProof(result, runtimeAgent, runtimeSlice);
406
418
  if (workerWorktree)
@@ -452,6 +464,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
452
464
  return result;
453
465
  },
454
466
  onSchedulerEvent: async ({ event, slots, state }) => {
467
+ await reapTimedOutAgentSessions();
455
468
  const paneBySlot = await readZellijPaneIdsBySlot(ledgerRoot);
456
469
  const enrichedSlots = slots.map((slot) => ({ ...slot, pane_id: paneBySlot.get(slot.slot_id) || null, launch_status: paneBySlot.has(slot.slot_id) ? 'launched' : slot.status }));
457
470
  await writeZellijRightLaneCockpit(ledgerRoot, { missionId, sessionName: `sks-${missionId}`, slots: enrichedSlots });
@@ -472,6 +485,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
472
485
  }
473
486
  }
474
487
  });
488
+ await reapTimedOutAgentSessions(true);
475
489
  await nativeCliSwarm.finalize();
476
490
  const parallelRuntimeProof = await writeParallelRuntimeProof(ledgerRoot, missionId, {
477
491
  requestedWorkers: Number(opts.agents || roster.agent_count || targetActiveSlots),
@@ -544,7 +558,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
544
558
  const stale = await detectStaleAgentSessions(ledgerRoot);
545
559
  if (!stale.ok)
546
560
  await appendAgentLedgerEvent(ledgerRoot, { agent_id: 'orchestrator', session_id: 'orchestrator', event_type: 'stale_sessions_detected', payload: stale });
547
- const timeoutKill = await killTimedOutAgentSessions(ledgerRoot);
561
+ const timeoutKill = await killTimedOutAgentSessions(ledgerRoot, Date.now(), { hardTimeoutMs: schedulerHardTimeoutMs });
548
562
  const recursion = await writeAgentRecursionGuardReport(ledgerRoot, results);
549
563
  const consensus = await writeAgentConsensus(ledgerRoot, results);
550
564
  const outputValidation = await writeAgentOutputValidationReport(ledgerRoot, results);
@@ -612,6 +626,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
612
626
  finalGptPatchStage
613
627
  });
614
628
  await writeAgentCodexCockpitArtifacts(dir, { missionId, projectHash: namespace.root_hash });
629
+ await enforceRetention(root, { afterRoute: true, completedMissionId: missionId, rotateLargeJsonl: true, lightweight: true }).catch(() => null);
615
630
  await setCurrent(root, { mission_id: missionId, mode: 'AGENT', phase: proof.ok ? 'AGENT_NATIVE_KERNEL_DONE' : 'AGENT_NATIVE_KERNEL_BLOCKED', native_agent_backend: backend, updated_at: nowIso() });
616
631
  return {
617
632
  schema: 'sks.agent-run.v1',
@@ -659,6 +674,12 @@ export async function runNativeAgentOrchestrator(opts = {}) {
659
674
  proof
660
675
  };
661
676
  }
677
+ function normalizeMissionHardTimeoutMs(opts = {}, route = '') {
678
+ const raw = Number(opts.hardTimeoutMs || opts.agentHardTimeoutMs || process.env.SKS_AGENT_HARD_TIMEOUT_MS || 0);
679
+ if (Number.isFinite(raw) && raw > 0)
680
+ return Math.max(1000, Math.min(Math.floor(raw), 24 * 60 * 60 * 1000));
681
+ return String(route || '').replace(/^\$/, '').toUpperCase() === 'NARUTO' ? 10 * 60 * 1000 : 30 * 60 * 1000;
682
+ }
662
683
  function withFinalGptPatchEnvelopes(results, patchEnvelopes = []) {
663
684
  const byAgent = new Map();
664
685
  for (const envelope of patchEnvelopes) {
@@ -7,6 +7,7 @@ import { buildCodexExecArgs } from '../codex/codex-cli-syntax-builder.js';
7
7
  export function buildCodexExecAgentArgs(agent, prompt, opts = {}) {
8
8
  const resultFile = opts.resultFile || defaultCodexResultFile(agent, opts);
9
9
  const sandbox = opts.workspaceWrite ? 'workspace-write' : 'read-only';
10
+ const noMcp = workerNoMcpDefault(agent, opts, sandbox);
10
11
  const args = buildCodexExecArgs({
11
12
  json: true,
12
13
  outputSchema: opts.schemaFile || path.join(packageRoot(), 'schemas/codex/agent-result.schema.json'),
@@ -15,6 +16,7 @@ export function buildCodexExecAgentArgs(agent, prompt, opts = {}) {
15
16
  skipGitRepoCheck: opts.skipGitRepoCheck !== false,
16
17
  profile: opts.profile ? String(opts.profile) : null,
17
18
  ignoreUserConfig: !opts.profile,
19
+ noMcp,
18
20
  ignoreRules: true,
19
21
  sandbox,
20
22
  serviceTier: opts.serviceTier || (opts.fastMode === false ? 'standard' : 'fast'),
@@ -49,6 +51,7 @@ export async function runCodexExecAgent(agent, slice, opts = {}) {
49
51
  fast_mode: fastPolicy.fast_mode,
50
52
  service_tier_passed_to_codex: codexArgsIncludeServiceTier(command.args, fastPolicy.service_tier),
51
53
  service_tier_cli_override_present: codexArgsIncludeServiceTier(command.args, fastPolicy.service_tier),
54
+ no_mcp: command.args.includes('--no-mcp'),
52
55
  output_schema_used: command.args.includes('--output-schema'),
53
56
  output_last_message_path: command.resultFile,
54
57
  agent_worker_env_injected: false,
@@ -78,6 +81,7 @@ export async function runCodexExecAgent(agent, slice, opts = {}) {
78
81
  fast_mode: fastPolicy.fast_mode,
79
82
  service_tier_passed_to_codex: codexArgsIncludeServiceTier(command.args, fastPolicy.service_tier),
80
83
  service_tier_cli_override_present: codexArgsIncludeServiceTier(command.args, fastPolicy.service_tier),
84
+ no_mcp: command.args.includes('--no-mcp'),
81
85
  output_schema_used: command.args.includes('--output-schema'),
82
86
  output_last_message_path: command.resultFile,
83
87
  agent_worker_env_injected: Object.keys(workerEnv).length > 0,
@@ -119,6 +123,26 @@ export async function runCodexExecAgent(agent, slice, opts = {}) {
119
123
  }
120
124
  return validateAgentWorkerResult({ mission_id: opts.missionId || opts.mission_id || '', agent_id: agent.id, session_id: agent.session_id, persona_id: agent.persona_id || agent.id, task_slice_id: slice?.id || '', status: result.code === 0 ? 'done' : 'failed', backend: 'codex-exec', summary: result.stdout.slice(-1000) || result.stderr.slice(-1000), artifacts: [command.resultFile, report], blockers: result.code === 0 ? ['codex_exec_output_last_message_missing_or_invalid'] : ['codex_exec_exit_' + result.code], confidence: 'verified_partial', unverified: result.code === 0 ? ['codex-exec stdout fallback; resultFile JSON missing or invalid'] : [], writes: [], source_intelligence_refs: agent.source_intelligence_refs || null, goal_mode_ref: agent.goal_mode_ref || null, verification: { status: result.code === 0 ? 'partial' : 'failed', checks: ['codex-exec-exit-code', 'codex-exec-process-report', 'codex-exec-output-last-message'] } });
121
125
  }
126
+ function workerNoMcpDefault(agent, opts = {}, sandbox = 'read-only') {
127
+ if (opts.noMcp !== undefined)
128
+ return opts.noMcp !== false;
129
+ if (sandbox === 'read-only')
130
+ return true;
131
+ const roleText = [
132
+ agent?.role,
133
+ agent?.persona_id,
134
+ agent?.required_role,
135
+ agent?.kind,
136
+ opts.role,
137
+ opts.kind
138
+ ].map((value) => String(value || '')).join(' ');
139
+ if (/\b(?:verify|verifier|verification|review|reviewer|qa|audit|read[-_ ]?only)\b/i.test(roleText))
140
+ return true;
141
+ const safeList = Array.isArray(opts.workerSafeMcpServers)
142
+ ? opts.workerSafeMcpServers
143
+ : String(process.env.SKS_WORKER_SAFE_MCP_SERVERS || '').split(',').map((item) => item.trim()).filter(Boolean);
144
+ return !(opts.allowWorkerMcp === true && safeList.length > 0);
145
+ }
122
146
  async function writeCodexProcessReport(root, agent, report) {
123
147
  const rel = path.join(agent.session_artifact_dir || path.join('sessions', agent.id), 'agent-process-report.json');
124
148
  await writeJsonAtomic(path.join(root, rel), { schema: 'sks.agent-process-report.v1', backend: 'codex-exec', agent_id: agent.id, session_id: agent.session_id, service_tier: report.service_tier || agent.service_tier || 'fast', fast_mode: report.fast_mode !== false, ...report });
@@ -122,13 +122,12 @@ export function applyFastModeToRoster(roster, policy) {
122
122
  };
123
123
  }
124
124
  export async function writeFastModePropagationProof(root, input = { policy: resolveFastModePolicy() }) {
125
- const workerFastReports = await collectNamedJson(root, 'worker-fast-mode.json');
126
125
  const workerProcessReports = await collectNamedJson(root, 'worker-process-report.json');
127
126
  const agentProcessReports = await collectNamedJson(root, 'agent-process-report.json');
128
127
  const zellijReports = await collectNamedJson(root, 'agent-zellij-report.json');
129
128
  const madReports = await collectNamedJson(root, 'mad-sks-worker-report.json');
130
129
  const defaultFastExpected = input.policy.fast_mode === true;
131
- const childReports = [...workerFastReports, ...workerProcessReports, ...agentProcessReports, ...zellijReports, ...madReports];
130
+ const childReports = [...workerProcessReports, ...agentProcessReports, ...zellijReports, ...madReports];
132
131
  const missingFast = defaultFastExpected
133
132
  ? childReports.filter((row) => row.json?.fast_mode !== true && row.json?.fast_mode !== 'true')
134
133
  : [];
@@ -137,7 +136,7 @@ export async function writeFastModePropagationProof(root, input = { policy: reso
137
136
  return value && value !== input.policy.service_tier;
138
137
  });
139
138
  const missingCliOverride = [];
140
- const workerMissing = defaultFastExpected && workerFastReports.length === 0 && workerProcessReports.length === 0
139
+ const workerMissing = defaultFastExpected && workerProcessReports.length === 0
141
140
  ? ['fast_mode_worker_reports_missing']
142
141
  : [];
143
142
  const report = {
@@ -149,7 +148,7 @@ export async function writeFastModePropagationProof(root, input = { policy: reso
149
148
  default_fast_mode: input.policy.default_fast_mode,
150
149
  service_tier: input.policy.service_tier,
151
150
  fast_mode: input.policy.fast_mode,
152
- worker_fast_report_count: workerFastReports.length,
151
+ worker_fast_report_count: 0,
153
152
  worker_process_report_count: workerProcessReports.length,
154
153
  codex_sdk_process_report_count: agentProcessReports.filter((row) => row.json?.backend === 'codex-sdk').length,
155
154
  process_report_count: agentProcessReports.filter((row) => row.json?.backend === 'process').length,
@@ -6,7 +6,7 @@ export async function writeNativeCliSessionProof(root, input = {}) {
6
6
  const swarm = await readJson(path.join(root, 'agent-native-cli-session-swarm.json'), null);
7
7
  const scheduler = await readJson(path.join(root, 'agent-scheduler-state.json'), null);
8
8
  const workerProcessReports = await collectNamedJson(root, 'worker-process-report.json');
9
- const workerCloseReports = await collectNamedJson(root, 'worker-terminal-close-report.json');
9
+ const workerCloseReports = workerProcessReports.filter((row) => row.json?.session_proof || row.json?.exit_code !== undefined);
10
10
  const workerHeartbeats = await collectHeartbeatCounts(root);
11
11
  const requestedAgents = Number(input.requestedAgents || swarm?.requested_agents || scheduler?.target_active_slots || 0);
12
12
  const targetActiveSlots = Number(input.targetActiveSlots || swarm?.target_active_slots || scheduler?.target_active_slots || requestedAgents);
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { ensureDir, nowIso, readJson, writeJsonAtomic, appendJsonl } from '../fsx.js';
2
+ import { appendJsonlBounded, ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
3
  import { scanAgentTextForRecursion } from './agent-recursion-guard.js';
4
4
  import { validateAgentWorkerResult } from './agent-worker-pipeline.js';
5
5
  import { resolveFastModePolicy } from './fast-mode-policy.js';
@@ -53,48 +53,49 @@ export async function runNativeCliWorker(input = {}) {
53
53
  worker_env: process.env.SKS_AGENT_WORKER === '1',
54
54
  violations: recursion.violations
55
55
  };
56
- await writeJsonAtomic(path.join(workerDir, 'worker-intake.json'), {
57
- schema: NATIVE_CLI_WORKER_SCHEMA,
58
- generated_at: nowIso(),
59
- parent_mission_id: String(intake.parent_mission_id || input.parentMissionId || intake.mission_id || ''),
60
- slot_id: String(agent.slot_id || input.slotId || ''),
61
- generation_index: Number(agent.generation_index || input.generationIndex || 1),
62
- task_id: String(slice.id || input.taskId || ''),
63
- persona_id: String(agent.persona_id || input.personaId || ''),
64
- lease_id: String(intake.lease_id || input.leaseId || ''),
65
- agent_root: agentRoot,
66
- main_repo_root: String(input.mainRepoRoot || intake.main_repo_root || worktree?.main_repo_root || agentRoot),
67
- cwd: workerCwd,
68
- worktree,
69
- worker_artifact_dir: workerDirRel,
70
- result_path: resultRel,
71
- heartbeat_path: heartbeatRel,
72
- patch_envelope_path: patchRel,
73
- backend,
74
- codex_task: {
75
- backend: backend === 'zellij' ? 'codex-sdk' : backend,
76
- output_schema_id: 'sks.agent-worker-result.v1',
77
- sandbox_policy: Array.isArray(slice.write_paths) && slice.write_paths.length > 0 ? 'workspace-write' : 'read-only',
78
- thread_policy: 'new_thread_per_generation',
56
+ if (process.env.SKS_DEBUG_ARTIFACTS === '1')
57
+ await writeJsonAtomic(path.join(workerDir, 'worker-intake.json'), {
58
+ schema: NATIVE_CLI_WORKER_SCHEMA,
59
+ generated_at: nowIso(),
60
+ parent_mission_id: String(intake.parent_mission_id || input.parentMissionId || intake.mission_id || ''),
61
+ slot_id: String(agent.slot_id || input.slotId || ''),
62
+ generation_index: Number(agent.generation_index || input.generationIndex || 1),
63
+ task_id: String(slice.id || input.taskId || ''),
64
+ persona_id: String(agent.persona_id || input.personaId || ''),
65
+ lease_id: String(intake.lease_id || input.leaseId || ''),
66
+ agent_root: agentRoot,
67
+ main_repo_root: String(input.mainRepoRoot || intake.main_repo_root || worktree?.main_repo_root || agentRoot),
68
+ cwd: workerCwd,
79
69
  worktree,
80
- cwd: workerCwd
81
- },
82
- service_tier: policy.service_tier,
83
- fast_mode: policy.fast_mode,
84
- source_intelligence_refs: agent.source_intelligence_refs || intake.source_intelligence_refs || null,
85
- goal_mode_ref: agent.goal_mode_ref || intake.goal_mode_ref || null,
86
- strategy_refs: slice.strategy_refs || intake.strategy_refs || null,
87
- no_recursive_orchestrator_guard: guard.ok
88
- });
70
+ worker_artifact_dir: workerDirRel,
71
+ result_path: resultRel,
72
+ heartbeat_path: heartbeatRel,
73
+ patch_envelope_path: patchRel,
74
+ backend,
75
+ codex_task: {
76
+ backend: backend === 'zellij' ? 'codex-sdk' : backend,
77
+ output_schema_id: 'sks.agent-worker-result.v1',
78
+ sandbox_policy: Array.isArray(slice.write_paths) && slice.write_paths.length > 0 ? 'workspace-write' : 'read-only',
79
+ thread_policy: 'new_thread_per_generation',
80
+ worktree,
81
+ cwd: workerCwd
82
+ },
83
+ service_tier: policy.service_tier,
84
+ fast_mode: policy.fast_mode,
85
+ source_intelligence_refs: agent.source_intelligence_refs || intake.source_intelligence_refs || null,
86
+ goal_mode_ref: agent.goal_mode_ref || intake.goal_mode_ref || null,
87
+ strategy_refs: slice.strategy_refs || intake.strategy_refs || null,
88
+ no_recursive_orchestrator_guard: guard.ok
89
+ });
89
90
  await workerTelemetry(agentRoot, intake, agent, slice, {
90
91
  eventType: 'task_started',
91
92
  status: 'running',
92
93
  backend,
93
94
  serviceTier: policy.service_tier,
94
- artifacts: [path.join(workerDirRel, 'worker-intake.json'), heartbeatRel],
95
+ artifacts: [heartbeatRel],
95
96
  logTail: String(slice.description || slice.title || slice.id || 'worker task started')
96
97
  });
97
- await appendJsonl(path.resolve(agentRoot, heartbeatRel), {
98
+ await appendJsonlBounded(path.resolve(agentRoot, heartbeatRel), {
98
99
  schema: 'sks.native-cli-worker-heartbeat.v1',
99
100
  ts: nowIso(),
100
101
  event: 'started',
@@ -104,7 +105,7 @@ export async function runNativeCliWorker(input = {}) {
104
105
  generation_index: agent.generation_index || null,
105
106
  fast_mode: policy.fast_mode,
106
107
  service_tier: policy.service_tier
107
- });
108
+ }, 2 * 1024 * 1024);
108
109
  await workerTelemetry(agentRoot, intake, agent, slice, {
109
110
  eventType: 'heartbeat',
110
111
  status: 'running',
@@ -113,18 +114,6 @@ export async function runNativeCliWorker(input = {}) {
113
114
  artifacts: [heartbeatRel],
114
115
  logTail: 'worker heartbeat started'
115
116
  });
116
- await writeJsonAtomic(path.join(workerDir, 'worker-fast-mode.json'), {
117
- schema: 'sks.native-cli-worker-fast-mode.v1',
118
- generated_at: nowIso(),
119
- ok: true,
120
- fast_mode: policy.fast_mode,
121
- service_tier: policy.service_tier,
122
- env: {
123
- SKS_FAST_MODE: process.env.SKS_FAST_MODE || null,
124
- SKS_SERVICE_TIER: process.env.SKS_SERVICE_TIER || null
125
- }
126
- });
127
- await writeJsonAtomic(path.join(workerDir, 'worker-recursion-guard.json'), guard);
128
117
  let noPatchReason = null;
129
118
  const progressTelemetry = startWorkerProgressTelemetry({
130
119
  agentRoot,
@@ -184,13 +173,12 @@ export async function runNativeCliWorker(input = {}) {
184
173
  backend,
185
174
  blockers: Array.isArray(slice.write_paths) && slice.write_paths.length && backend !== 'fake' ? ['write_capable_no_patch_envelope'] : []
186
175
  };
187
- await writeJsonAtomic(path.join(workerDir, 'worker-no-patch-reason.json'), noPatchReason);
188
176
  await workerTelemetry(agentRoot, intake, agent, slice, {
189
177
  eventType: 'artifact_written',
190
178
  status: 'running',
191
179
  backend,
192
180
  serviceTier: policy.service_tier,
193
- artifacts: [path.join(workerDirRel, 'worker-no-patch-reason.json')],
181
+ artifacts: [resultRel],
194
182
  blockers: noPatchReason.blockers || [],
195
183
  logTail: noPatchReason.reason
196
184
  });
@@ -203,6 +191,9 @@ export async function runNativeCliWorker(input = {}) {
203
191
  generated_at: nowIso(),
204
192
  ok: guard.ok,
205
193
  backend,
194
+ fast_mode: policy.fast_mode,
195
+ service_tier: policy.service_tier,
196
+ codex_desktop_service_tier: policy.codex_desktop_service_tier,
206
197
  cwd: workerCwd,
207
198
  worktree,
208
199
  agent_id: agent.id,
@@ -213,8 +204,6 @@ export async function runNativeCliWorker(input = {}) {
213
204
  ppid: process.ppid,
214
205
  process_id: process.pid,
215
206
  command_line: redactCommandLine(process.argv),
216
- fast_mode: policy.fast_mode,
217
- service_tier: policy.service_tier,
218
207
  sdk_thread_id: routed.report?.sdk_thread_id || null,
219
208
  sdk_run_id: routed.report?.sdk_run_id || null,
220
209
  stream_event_count: Number(routed.report?.stream_event_count || 0),
@@ -224,21 +213,32 @@ export async function runNativeCliWorker(input = {}) {
224
213
  backend_child_execution: routed.report.child_process_ids.length > 0 || Boolean(routed.report?.sdk_thread_id) || backend === 'fake' || backend === 'ollama',
225
214
  recursion_guard_env: process.env.SKS_DISABLE_ROUTE_RECURSION === '1',
226
215
  worker_env: process.env.SKS_AGENT_WORKER === '1',
216
+ fast_mode_report: {
217
+ ok: true,
218
+ fast_mode: policy.fast_mode,
219
+ service_tier: policy.service_tier,
220
+ env: {
221
+ SKS_FAST_MODE: process.env.SKS_FAST_MODE || null,
222
+ SKS_SERVICE_TIER: process.env.SKS_SERVICE_TIER || null
223
+ }
224
+ },
225
+ recursion_guard: guard,
226
+ session_proof: {
227
+ ok: guard.ok,
228
+ session_id: agent.session_id,
229
+ slot_id: agent.slot_id || null,
230
+ generation_index: agent.generation_index || null,
231
+ artifact_dir: workerDirRel,
232
+ patch_envelope: patchEnvelopes.length ? patchRel : null
233
+ },
227
234
  exit_code: guard.ok ? 0 : 1
228
235
  };
229
236
  await writeJsonAtomic(path.join(workerDir, 'worker-process-report.json'), report);
230
237
  const artifacts = [
231
- path.join(workerDirRel, 'worker-intake.json'),
232
- heartbeatRel,
233
- path.join(workerDirRel, 'worker-process-report.json'),
234
- routed.reportRel,
235
- ...routed.result.artifacts.filter((artifact) => artifact.includes('codex-worker-') || artifact.includes('agent-process-report') || artifact.includes('agent-zellij-report')),
236
238
  resultRel,
237
- patchEnvelopes.length ? patchRel : path.join(workerDirRel, 'worker-no-patch-reason.json'),
238
- path.join(workerDirRel, 'worker-terminal-close-report.json'),
239
- path.join(workerDirRel, 'worker-fast-mode.json'),
240
- path.join(workerDirRel, 'worker-recursion-guard.json'),
241
- path.join(workerDirRel, 'worker-session-proof.json')
239
+ path.join(workerDirRel, 'worker-process-report.json'),
240
+ heartbeatRel,
241
+ ...(patchEnvelopes.length ? [patchRel] : [])
242
242
  ];
243
243
  const result = validateAgentWorkerResult({
244
244
  ...routed.result,
@@ -268,7 +268,7 @@ export async function runNativeCliWorker(input = {}) {
268
268
  zellij_child_report: routed.result.zellij_child_report,
269
269
  model_authored_patch_envelopes: patchEnvelopes.some((envelope) => envelope.source === 'model_authored'),
270
270
  fixture_patch_envelopes: patchEnvelopes.some((envelope) => envelope.source === 'fixture'),
271
- ...(!patchEnvelopes.length ? { no_patch_reason: noPatchReason || await readJson(path.join(workerDir, 'worker-no-patch-reason.json'), null) } : {}),
271
+ ...(!patchEnvelopes.length ? { no_patch_reason: noPatchReason } : {}),
272
272
  source_intelligence_refs: agent.source_intelligence_refs || intake.source_intelligence_refs || null,
273
273
  goal_mode_ref: agent.goal_mode_ref || intake.goal_mode_ref || null,
274
274
  verification: { status: guard.ok && routed.result.status === 'done' ? 'passed' : 'failed', checks: [...(routed.result.verification?.checks || []), 'native-cli-worker-process', 'worker-artifact-contract', 'fast-mode-policy', 'native-worker-backend-router'] },
@@ -284,40 +284,14 @@ export async function runNativeCliWorker(input = {}) {
284
284
  blockers: result.blockers || [],
285
285
  logTail: result.summary || ''
286
286
  });
287
- await writeJsonAtomic(path.join(workerDir, 'worker-session-proof.json'), {
288
- schema: 'sks.native-cli-worker-session-proof.v1',
289
- generated_at: nowIso(),
290
- ok: result.status === 'done',
291
- session_id: result.session_id,
292
- slot_id: agent.slot_id || null,
293
- generation_index: agent.generation_index || null,
294
- process_id: process.pid,
295
- main_repo_root: String(input.mainRepoRoot || intake.main_repo_root || worktree?.main_repo_root || agentRoot),
296
- cwd: workerCwd,
297
- worktree,
298
- artifact_dir: workerDirRel,
299
- patch_envelope: patchEnvelopes.length ? patchRel : null,
300
- no_patch_reason: patchEnvelopes.length ? null : path.join(workerDirRel, 'worker-no-patch-reason.json'),
301
- backend_router_report: routed.reportRel,
302
- backend_child_process_ids: routed.report.child_process_ids,
303
- sdk_thread_id: routed.report?.sdk_thread_id || null,
304
- sdk_run_id: routed.report?.sdk_run_id || null,
305
- stream_event_count: Number(routed.report?.stream_event_count || 0),
306
- structured_output_valid: routed.report?.structured_output_valid === true,
307
- model_authored_patch_envelopes: patchEnvelopes.some((envelope) => envelope.source === 'model_authored'),
308
- fixture_patch_envelopes: patchEnvelopes.some((envelope) => envelope.source === 'fixture'),
309
- fast_mode: policy.fast_mode,
310
- service_tier: policy.service_tier,
311
- blockers: result.blockers
312
- });
313
- await appendJsonl(path.resolve(agentRoot, heartbeatRel), {
287
+ await appendJsonlBounded(path.resolve(agentRoot, heartbeatRel), {
314
288
  schema: 'sks.native-cli-worker-heartbeat.v1',
315
289
  ts: nowIso(),
316
290
  event: 'finished',
317
291
  pid: process.pid,
318
292
  session_id: agent.session_id,
319
293
  status: result.status
320
- });
294
+ }, 2 * 1024 * 1024);
321
295
  await workerTelemetry(agentRoot, intake, agent, slice, {
322
296
  eventType: 'heartbeat',
323
297
  status: result.status === 'done' ? 'completed' : 'failed',
@@ -327,32 +301,18 @@ export async function runNativeCliWorker(input = {}) {
327
301
  blockers: result.blockers || [],
328
302
  logTail: 'worker heartbeat finished'
329
303
  });
330
- await writeJsonAtomic(path.join(workerDir, 'worker-terminal-close-report.json'), {
331
- schema: 'sks.native-cli-worker-terminal-close-report.v1',
332
- generated_at: nowIso(),
333
- ok: result.status === 'done',
334
- session_id: result.session_id,
335
- slot_id: agent.slot_id || null,
336
- generation_index: agent.generation_index || null,
337
- process_id: process.pid,
338
- exit_code: result.status === 'done' ? 0 : 1,
339
- fast_mode: policy.fast_mode,
340
- service_tier: policy.service_tier,
341
- blockers: result.blockers
342
- });
343
304
  return result;
344
305
  }
345
306
  function delay(ms) {
346
307
  return new Promise((resolve) => setTimeout(resolve, ms));
347
308
  }
348
309
  function startWorkerProgressTelemetry(input) {
349
- const parsed = Number(process.env.SKS_ZELLIJ_WORKER_PROGRESS_MS || 2000);
350
- const intervalMs = Math.max(500, Number.isFinite(parsed) ? Math.floor(parsed) : 2000);
310
+ const parsed = Number(process.env.SKS_ZELLIJ_WORKER_PROGRESS_MS || 10000);
311
+ const intervalMs = Math.max(1000, Number.isFinite(parsed) ? Math.floor(parsed) : 10000);
351
312
  let tick = 0;
352
313
  const timer = setInterval(() => {
353
314
  tick += 1;
354
- const progress = { done: tick, total: 0, label: 'backend running' };
355
- appendJsonl(path.resolve(input.agentRoot, input.heartbeatRel), {
315
+ appendJsonlBounded(path.resolve(input.agentRoot, input.heartbeatRel), {
356
316
  schema: 'sks.native-cli-worker-heartbeat.v1',
357
317
  ts: nowIso(),
358
318
  event: 'progress',
@@ -360,15 +320,14 @@ function startWorkerProgressTelemetry(input) {
360
320
  session_id: input.agent.session_id,
361
321
  slot_id: input.agent.slot_id || null,
362
322
  generation_index: input.agent.generation_index || null,
363
- progress
364
- }).catch(() => undefined);
323
+ progress: null
324
+ }, 2 * 1024 * 1024).catch(() => undefined);
365
325
  workerTelemetry(input.agentRoot, input.intake, input.agent, input.slice, {
366
- eventType: 'task_progress',
326
+ eventType: 'heartbeat',
367
327
  status: 'running',
368
328
  backend: input.backend,
369
329
  serviceTier: input.serviceTier,
370
330
  artifacts: [input.heartbeatRel],
371
- progress,
372
331
  logTail: `backend running ${tick}`
373
332
  }).catch(() => undefined);
374
333
  }, intervalMs);
@@ -435,7 +394,8 @@ async function workerTelemetry(agentRoot, intake, agent, slice, input) {
435
394
  service_tier: input.serviceTier,
436
395
  worktree_id: agent.worktree?.id || slice.worktree?.id || intake.worktree?.id || null,
437
396
  worktree_path: agent.worktree?.path || slice.worktree?.path || intake.worktree?.path || null,
438
- task_title: String(slice.description || slice.title || slice.id || 'worker task'),
397
+ task_id: String(slice.id || 'worker-task'),
398
+ ...(input.eventType === 'task_started' ? { task_title: String(slice.description || slice.title || slice.id || 'worker task') } : {}),
439
399
  current_file: firstString([slice.write_paths?.[0], slice.readonly_paths?.[0], slice.input_files?.[0]]) || null,
440
400
  ...(input.progress ? { progress: input.progress } : {}),
441
401
  artifact_paths: input.artifacts || [],