@ngockhoale/ukit 3.4.68 → 3.4.70

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 (25) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/manifests/documentation.yaml +88 -0
  3. package/manifests/platform.full.yaml +13 -1
  4. package/package.json +5 -1
  5. package/scripts/install/module-dependency-closure.mjs +53 -0
  6. package/scripts/install/sync-installed-mirror.mjs +17 -4
  7. package/scripts/release/candidate-state.mjs +31 -0
  8. package/scripts/release/verify-release.mjs +60 -6
  9. package/src/core/buildPlan.js +2 -0
  10. package/src/core/executionContracts.js +1 -309
  11. package/template_project/.claude/commands/ukit/handoff-create.md +41 -1
  12. package/template_project/.claude/hooks/handoff-model-guard.sh +459 -1
  13. package/template_project/.claude/hooks/skill-router.sh +585 -357
  14. package/template_project/.claude/ukit/index/publication-intent.mjs +528 -0
  15. package/template_project/.claude/ukit/index/route-resolver.mjs +1 -290
  16. package/template_project/.claude/ukit/index/route-task.mjs +13 -1
  17. package/template_project/.claude/ukit/runtime/async-lock.mjs +3 -3
  18. package/template_project/.claude/ukit/runtime/block-breaker.mjs +232 -1
  19. package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +46 -8
  20. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +161 -56
  21. package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +51 -1
  22. package/template_project/.claude/ukit/runtime/operation-evidence.mjs +34 -10
  23. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +357 -44
  24. package/template_project/.omp/hooks/pre/ukit-bridge.js +50 -26
  25. package/scripts/dev/capture-omp-stall.mjs +0 -246
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.70 - 2026-10-26
6
+
7
+ **Task-owned completion evidence and bounded reminder recovery; no native-host certification.**
8
+
9
+ - Classification: configuration comparisons, read-only output specifications and owner-approval reminders do not inherit mutation or verification debt. Explicit verification and publication orders retain their own obligations; private Git scope and later affirmative orders remain distinct. Descriptive acceptance-review prose — subordinator continuation fragments ("…, including source fixes F-3") and non-imperative goal/acceptance lines ("Final product goal: … normal ukit update pass") — no longer routes read-only reviews as implementation orders; any imperative fragment inside such a line still enforces write+verification.
10
+ - Completion: preserve truthful blocked outcomes, scoped exhausted-denial reconciliation and native Git/remote evidence. Launch acknowledgements, missing exit proof, masked commands, foreign receipts and stale parity cannot prove success. Docs-only Handoff creation honors the owner's explicit Git publication scope.
11
+ - Reminder recovery: reapply classified evidence obligations even when the task-kind signature is unchanged. Repeated read-only reminders need no artificial checkpoint verification; owned external blockers remain blocked rather than successful. No permission reset, receipt bypass or safety relaxation.
12
+ - Release verification: bind mandatory phases to candidate bytes and fail on drift; run existing live-documentation checks before expensive gates and retain external per-run result metadata. Mandatory lanes, artifact budgets and timeouts are unchanged.
13
+ - Evidence limits: targeted regressions and independent reviews cover shared Claude/OMP sources and fixtures; Codex remains advisory where no native hook exists. Historical failed verifier runs remain failed. The review integration fixture now has a measured, first-case-only 15s allowance after bounded child-latency reproduction; global/runtime timeouts are unchanged. Discovery deadline testing waits for the intended filesystem phase instead of racing startup I/O. No speculative performance fix or exact historical slowdown attribution is claimed.
14
+
15
+ ## 3.4.69 - 2026-10-25
16
+
17
+ **Narrow shared-lock cancellation repair; no new unattended-Handoff certification.**
18
+
19
+ - Fix: cancellation after a successful lock owner stamp now passes through token-checked, best-effort release before returning `aborted`; the callback remains unexecuted. Deterministic regression verifies lock absence and immediate reacquisition.
20
+ - Tests: managed Handoff template cases pin owner-byte preservation, exact sidecar bytes/provenance and no-write aligned behavior. Preflight fixtures now use an independent temporary Git repository with ownership-checked cleanup, avoiding default dirty-worktree/sandbox accumulation without touching retained evidence.
21
+ - Verification: independent original and cleanup-only reviews approved with minor findings; full release verifier passed (core 10,741 passed / three skipped, isolated handoff 351/351, artifact 39/39). Cleanup review retains a non-blocking concurrency-sensitive source-inventory assertion and fail-loud fixture cleanup caveats.
22
+ - Limits unchanged: native isolation dispatch, clean unattended lifecycle/native-host acceptance, historical `ukitcmd25` loss and unidentified `79/1` remain unresolved. This fix is not established as the cause of those incidents. No 3.4.68 live-gate waiver is inherited or live acceptance claimed.
23
+
5
24
  ## 3.4.68 - 2026-10-25
6
25
 
7
26
  **Stable hardening release; outstanding live acceptance explicitly deferred by the owner for the internal six-person team. This is not certification of unattended Handoff.**
@@ -4,6 +4,17 @@ description: >-
4
4
  Machine-readable ownership/source registry for every documentation surface.
5
5
  Hand-authored per SPEC §7; enforced by tests/consistency/docRegistry.test.js.
6
6
  entries:
7
+ - id: systematic-loop-audit-2026-10-26
8
+ path: docs/analysis/SYSTEMATIC-LOOP-AUDIT-2026-10-26.md
9
+ class: canonical
10
+ audience: [maintainer, agent]
11
+ owner: release
12
+ source_of_truth: docs/analysis/SYSTEMATIC-LOOP-AUDIT-2026-10-26.md
13
+ merge_strategy: none
14
+ load_policy: on-demand
15
+ validation: [manual]
16
+ archive_policy: never
17
+ notes: bounded audit inventory and repair-round ledger; no additional full-verifier allowance
7
18
  # ── Repo root ────────────────────────────────────────────────────────────
8
19
  - id: root-claude-md
9
20
  path: CLAUDE.md
@@ -451,6 +462,72 @@ entries:
451
462
  validation: [manual]
452
463
  archive_policy: replace-with-snapshot
453
464
  notes: Phase A read-only concurrency audit; provisional metrics labelled, pilot not run
465
+ - id: docs-status-history-2026-10-25
466
+ path: docs/STATUS-HISTORY-2026-10-25.md
467
+ class: archive
468
+ audience: [maintainer]
469
+ owner: release
470
+ source_of_truth: docs/STATUS-HISTORY-2026-10-25.md
471
+ merge_strategy: none
472
+ load_policy: on-demand
473
+ validation: [manual]
474
+ archive_policy: never
475
+ notes: Superseded STATUS checkpoints moved verbatim from docs/STATUS.md (Round4)
476
+ - id: docs-analysis-round4-defect-closure
477
+ path: docs/analysis/ROUND4-DEFECT-CLOSURE.md
478
+ class: canonical
479
+ audience: [maintainer]
480
+ owner: release
481
+ source_of_truth: docs/analysis/ROUND4-DEFECT-CLOSURE.md
482
+ merge_strategy: none
483
+ load_policy: on-demand
484
+ validation: [manual]
485
+ archive_policy: never
486
+ notes: Round4 working defect-closure register; not an acceptance packet
487
+ - id: docs-analysis-round4-reviewer-failures
488
+ path: docs/analysis/ROUND4-REVIEWER-FAILURES.md
489
+ class: canonical
490
+ audience: [maintainer]
491
+ owner: release
492
+ source_of_truth: docs/analysis/ROUND4-REVIEWER-FAILURES.md
493
+ merge_strategy: none
494
+ load_policy: on-demand
495
+ validation: [manual]
496
+ archive_policy: never
497
+ notes: All 95 original reviewer failures with candidate-assessed dispositions; independent acceptance still required
498
+ - id: docs-analysis-round4-systemic-intent
499
+ path: docs/analysis/ROUND4-SYSTEMIC-INTENT.md
500
+ class: canonical
501
+ audience: [maintainer]
502
+ owner: release
503
+ source_of_truth: docs/analysis/ROUND4-SYSTEMIC-INTENT.md
504
+ merge_strategy: none
505
+ load_policy: on-demand
506
+ validation: [manual]
507
+ archive_policy: never
508
+ notes: Round4 systemic intent model and acceptance scope
509
+ - id: docs-analysis-round4-terminal-core-diagnostics
510
+ path: docs/analysis/ROUND4-TERMINAL-CORE-DIAGNOSTICS.md
511
+ class: archive
512
+ audience: [maintainer]
513
+ owner: release
514
+ source_of_truth: docs/analysis/ROUND4-TERMINAL-CORE-DIAGNOSTICS.md
515
+ merge_strategy: none
516
+ load_policy: on-demand
517
+ validation: [manual]
518
+ archive_policy: never
519
+ notes: Round4 terminal Core diagnostics evidence
520
+ - id: docs-debug-ukit-3431-debug-audit
521
+ path: docs/debug/UKIT-3.4.31-DEBUG-AUDIT.md
522
+ class: archive
523
+ audience: [maintainer]
524
+ owner: release
525
+ source_of_truth: docs/debug/UKIT-3.4.31-DEBUG-AUDIT.md
526
+ merge_strategy: none
527
+ load_policy: on-demand
528
+ validation: [manual]
529
+ archive_policy: never
530
+ notes: Unreleased 3.4.31 debug audit (2026-10-11); untracked file of unrecorded provenance, registered so the coverage gate sees it
454
531
  - id: docs-analysis-concurrency-checkpoint
455
532
  path: docs/analysis/CONCURRENCY-CHECKPOINT-2026-10-22.md
456
533
  class: canonical
@@ -660,6 +737,17 @@ entries:
660
737
  validation: [manual]
661
738
  archive_policy: never
662
739
  notes: Native planning Write permission evidence, authorized configuration provenance and unresolved live acceptance boundaries
740
+ - id: docs-analysis-handoff-create-git-completion
741
+ path: docs/analysis/HANDOFF-CREATE-GIT-COMPLETION.md
742
+ class: canonical
743
+ audience: [maintainer]
744
+ owner: product
745
+ source_of_truth: docs/analysis/HANDOFF-CREATE-GIT-COMPLETION.md
746
+ merge_strategy: none
747
+ load_policy: on-demand
748
+ validation: [manual]
749
+ archive_policy: never
750
+ notes: Create-scope Git authorization, operation evidence, task-owned denial reconciliation and release verification boundaries
663
751
  - id: docs-analysis-orchestration-final-verification
664
752
  path: docs/analysis/ORCHESTRATION-FINAL-VERIFICATION.md
665
753
  class: archive
@@ -1535,7 +1535,8 @@ items:
1535
1535
  type: config
1536
1536
  sourceTemplate: .claude/ukit/runtime
1537
1537
  targetPath: .claude/ukit/runtime
1538
- requires: []
1538
+ requires:
1539
+ - ukit-index-publication-intent-script
1539
1540
  mergeStrategy: overwrite_with_backup
1540
1541
  variables: []
1541
1542
  enabledByDefault: true
@@ -1856,11 +1857,22 @@ items:
1856
1857
  packs:
1857
1858
  - core
1858
1859
 
1860
+ - id: ukit-index-publication-intent-script
1861
+ type: config
1862
+ sourceTemplate: .claude/ukit/index/publication-intent.mjs
1863
+ targetPath: .claude/ukit/index/publication-intent.mjs
1864
+ mergeStrategy: overwrite_with_backup
1865
+ variables: []
1866
+ enabledByDefault: true
1867
+ packs:
1868
+ - core
1869
+
1859
1870
  - id: ukit-index-route-resolver-script
1860
1871
  type: config
1861
1872
  sourceTemplate: .claude/ukit/index/route-resolver.mjs
1862
1873
  targetPath: .claude/ukit/index/route-resolver.mjs
1863
1874
  requires:
1875
+ - ukit-index-publication-intent-script
1864
1876
  - ukit-index-core-lib
1865
1877
  - ukit-index-cache-utils-script
1866
1878
  - ukit-index-route-catalog-script
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.68",
3
+ "version": "3.4.70",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,6 +30,10 @@
30
30
  "bin/",
31
31
  "src/",
32
32
  "scripts/",
33
+ "!scripts/dev/benchmark-verification-currency.mjs",
34
+ "!scripts/dev/capture-omp-stall.mjs",
35
+ "!scripts/install/generate-semantic-engine.mjs",
36
+ "!scripts/release/progress-reporter.mjs",
33
37
  "!scripts/audit/c100/attribution-gap.mjs",
34
38
  "!scripts/audit/c100/implementer-attribution.mjs",
35
39
  "!scripts/audit/c100/trees-lifetimes.mjs",
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // Static specifiers plus URL-resolved dynamic imports (`new URL('./x.mjs', import.meta.url)`), which runtimes use for
5
+ // lazy safety modules. Only literal module-file URLs are edges: assets (.json) and computed specifiers are not.
6
+ function relativeModuleDependencies(text) {
7
+ const imports = /(?:\b(?:import|export)\s+(?:[^;'"\n]*?\s+from\s*)?|\bimport\s*\(\s*)['"](\.{1,2}\/[^'"\n]+)['"]/g;
8
+ const urls = /\bnew\s+URL\(\s*['"](\.{1,2}\/[^'"\n]+\.[cm]?js)['"]\s*,\s*import\.meta\.url/g;
9
+ return [...new Set([...text.matchAll(imports), ...text.matchAll(urls)].map((match) => match[1]))];
10
+ }
11
+
12
+ export function validateModuleClosureSync(entries) {
13
+ const targets = new Map(entries.filter((entry) => entry.sourcePath)
14
+ .map((entry) => [path.resolve(entry.targetPath), path.resolve(entry.sourcePath)]));
15
+ for (const entry of entries) {
16
+ if (!entry.sourcePath || !/\.[cm]?js$/.test(entry.sourcePath)) continue;
17
+ const text = fs.readFileSync(entry.sourcePath, 'utf8');
18
+ for (const dependency of relativeModuleDependencies(text)) {
19
+ const source = path.resolve(path.dirname(entry.sourcePath), dependency);
20
+ const target = path.resolve(path.dirname(entry.targetPath), dependency);
21
+ if (targets.get(target) !== source) {
22
+ throw new Error(`Runtime dependency missing or misrelocated: ${entry.sourcePath} -> ${dependency} (expected ${target})`);
23
+ }
24
+ }
25
+ }
26
+ }
27
+
28
+ // Relocation preserves relative module topology. Missing declared dependencies
29
+ // are packaging errors, never an excuse to disable a runtime safety check.
30
+ export function copyModuleClosureSync(source, destination, visited = new Set()) {
31
+ source = path.resolve(source);
32
+ destination = path.resolve(destination);
33
+ const key = `${source}\0${destination}`;
34
+ if (visited.has(key)) return;
35
+ visited.add(key);
36
+ const bytes = fs.readFileSync(source);
37
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
38
+ fs.writeFileSync(destination, bytes);
39
+ if (!/\.[cm]?js$/.test(source)) return;
40
+ const text = bytes.toString('utf8');
41
+ for (const dependency of relativeModuleDependencies(text)) {
42
+ copyModuleClosureSync(path.resolve(path.dirname(source), dependency),
43
+ path.resolve(path.dirname(destination), dependency), visited);
44
+ }
45
+ }
46
+
47
+ export function copyRuntimeClosureSync(sourceDirectory, destinationDirectory) {
48
+ const visited = new Set();
49
+ for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
50
+ if (!entry.isFile()) continue;
51
+ copyModuleClosureSync(path.join(sourceDirectory, entry.name), path.join(destinationDirectory, entry.name), visited);
52
+ }
53
+ }
@@ -140,6 +140,15 @@ async function collectMirrorOnlyPaths({ plan, projectRoot, templatesRoot }) {
140
140
  return [...new Set(mirrorOnly)].sort();
141
141
  }
142
142
 
143
+ // Hooks write their own state under `<managed dir>/.ukit/storage/` (stop-coordinator, task-watchdog,
144
+ // observability). That is protected runtime state, not template drift: it is reported for transparency but never
145
+ // fails the check, is never rewritten, and is never offered for deletion. Any other unshipped file stays mirror-only.
146
+ const RUNTIME_STATE_SEGMENT = '/.ukit/storage/';
147
+ function splitRuntimeState(paths) {
148
+ const runtimeState = paths.filter((p) => `/${p}`.includes(RUNTIME_STATE_SEGMENT));
149
+ return { mirrorOnly: paths.filter((p) => !runtimeState.includes(p)), runtimeState };
150
+ }
151
+
143
152
  async function main() {
144
153
  const args = process.argv.slice(2);
145
154
  const unknown = args.filter((arg) => arg !== '--check');
@@ -192,11 +201,11 @@ async function main() {
192
201
  .map((entry) => ({ entry, relativePath: toRelative(projectRoot, entry.targetPath) }))
193
202
  .sort((a, b) => a.relativePath.localeCompare(b.relativePath));
194
203
 
195
- const mirrorOnly = await collectMirrorOnlyPaths({
204
+ const { mirrorOnly, runtimeState } = splitRuntimeState(await collectMirrorOnlyPaths({
196
205
  plan,
197
206
  projectRoot,
198
207
  templatesRoot: pathConfig.templatesRoot,
199
- });
208
+ }));
200
209
 
201
210
  if (checkOnly) {
202
211
  for (const { entry, relativePath } of drift) {
@@ -205,15 +214,19 @@ async function main() {
205
214
  for (const relativePath of mirrorOnly) {
206
215
  console.log(`MIRROR-ONLY ${relativePath}`);
207
216
  }
217
+ for (const relativePath of runtimeState) {
218
+ console.log(`RUNTIME-STATE ${relativePath} (protected; not drift)`);
219
+ }
208
220
 
221
+ const stateNote = runtimeState.length ? `, ${runtimeState.length} protected runtime-state file(s) ignored` : '';
209
222
  if (drift.length === 0 && mirrorOnly.length === 0) {
210
- console.log(`ukit mirror check: clean (${managedRelativePaths.size} scoped targets)`);
223
+ console.log(`ukit mirror check: clean (${managedRelativePaths.size} scoped targets${stateNote})`);
211
224
  return 0;
212
225
  }
213
226
 
214
227
  console.log(
215
228
  `ukit mirror check: ${drift.length} drifted, ${mirrorOnly.length} mirror-only `
216
- + `(${managedRelativePaths.size} scoped targets)`,
229
+ + `(${managedRelativePaths.size} scoped targets${stateNote})`,
217
230
  );
218
231
  return 1;
219
232
  }
@@ -0,0 +1,31 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { lstatSync, readFileSync, readlinkSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import path from 'node:path';
5
+
6
+ // Runtime receipts are outputs of verification, not release candidate inputs.
7
+ // Canonical template_project runtime sources are deliberately NOT excluded.
8
+ const mutable = /^(?:\.ukit\/storage\/|\.claude\/ukit\/runtime\/\.ukit\/storage\/|\.cache\/|node_modules\/)/;
9
+
10
+ export function candidateFingerprint(root) {
11
+ const files = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
12
+ cwd: root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, timeout: 30_000,
13
+ }).split('\0').filter((file) => file && !mutable.test(file));
14
+ const hash = createHash('sha256');
15
+ for (const file of [...new Set(files)].sort()) {
16
+ hash.update(JSON.stringify(file));
17
+ const absolute = path.join(root, file);
18
+ let stat;
19
+ try { stat = lstatSync(absolute); } catch (error) {
20
+ if (error.code !== 'ENOENT') throw error;
21
+ hash.update('\0deleted\0');
22
+ continue;
23
+ }
24
+ hash.update(`\0${stat.mode}\0`);
25
+ if (stat.isSymbolicLink()) hash.update(`link\0${readlinkSync(absolute)}`);
26
+ else if (stat.isFile()) hash.update(readFileSync(absolute));
27
+ else throw new Error(`Unsupported candidate entry: ${file}`);
28
+ hash.update('\0');
29
+ }
30
+ return hash.digest('hex');
31
+ }
@@ -1,8 +1,9 @@
1
- import { spawn } from 'node:child_process';
2
- import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
1
+ import { spawn, execFileSync } from 'node:child_process';
2
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdtempSync } from 'node:fs';
3
3
  import crypto from 'node:crypto';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { candidateFingerprint } from './candidate-state.mjs';
6
7
 
7
8
  const rootDir = process.cwd();
8
9
  const npmCacheDir = path.join(os.tmpdir(), 'ukit-npm-cache');
@@ -75,6 +76,12 @@ const steps = [
75
76
  args: ['docs:render:check'],
76
77
  env: process.env,
77
78
  },
79
+ {
80
+ label: 'Live documentation preflight (headers, budgets, current status)',
81
+ command: 'yarn',
82
+ args: ['vitest', 'run', 'tests/consistency/liveStateDocs.test.js', 'tests/consistency/advisorStatusDocs.test.js'],
83
+ env: process.env,
84
+ },
78
85
  {
79
86
  label: 'Consistency suite (template<->active sync, watchdog coverage, render parity)',
80
87
  command: 'yarn',
@@ -117,17 +124,46 @@ const steps = [
117
124
  },
118
125
  ];
119
126
 
127
+ const runDirectory = mkdtempSync(path.join(os.tmpdir(), 'ukit-release-run-'));
128
+ const candidate = candidateFingerprint(rootDir);
129
+ const run = { pid: process.pid, candidate, startedAt: new Date().toISOString(), phase: null, result: 'running', exitCode: null };
130
+ const persistRun = () => writeFileSync(path.join(runDirectory, 'result.json'), `${JSON.stringify(run, null, 2)}\n`);
131
+ persistRun();
132
+ console.log(`[release:verify] Candidate ${candidate}; run evidence ${runDirectory}`);
133
+ process.on('exit', (code) => {
134
+ run.exitCode = code;
135
+ if (run.result === 'running') run.result = 'failed';
136
+ run.finishedAt = new Date().toISOString();
137
+ persistRun();
138
+ });
139
+ function assertCandidate() {
140
+ if (candidateFingerprint(rootDir) !== candidate) {
141
+ console.error('[release:verify] FAILED: candidate changed during verification; no PASS applies to the current bytes.');
142
+ process.exit(1);
143
+ }
144
+ }
145
+
120
146
  for (const step of steps) {
147
+ assertCandidate();
148
+ run.phase = step.label;
149
+ run.phaseStartedAt = new Date().toISOString();
150
+ run.phaseResult = null;
151
+ step.env = { ...step.env, UKIT_VERIFY_PROGRESS_FILE: path.join(runDirectory, `phase-${steps.indexOf(step)}-progress.json`) };
152
+ persistRun();
121
153
  console.log(`\n[release:verify] ${step.label}`);
122
154
  console.log(`[release:verify] > ${step.command} ${step.args.join(' ')}`);
123
155
 
124
156
  const exitCode = await runStep(step);
157
+ assertCandidate();
125
158
  if (exitCode !== 0) {
126
159
  console.error(`[release:verify] FAILED at step: ${step.label}`);
127
160
  process.exit(exitCode ?? 1);
128
161
  }
129
162
  }
130
163
 
164
+ assertCandidate();
165
+ run.result = 'passed';
166
+ persistRun();
131
167
  console.log('\n[release:verify] All release checks passed.');
132
168
 
133
169
  // BUG-C22-20: bounded spawn — an armed watchdog SIGKILLs the child's whole
@@ -138,6 +174,16 @@ function spawnBounded(command, args, opts, timeoutMs) {
138
174
  child.ukitTimedOut = false;
139
175
  const watchdog = setTimeout(() => {
140
176
  child.ukitTimedOut = true;
177
+ // One bounded native snapshot before termination, never a polling watcher.
178
+ // ps contains no argv/environment (credentials); unsupported hosts record the error.
179
+ if (!postPublish && opts.env?.UKIT_VERIFY_PROGRESS_FILE) try {
180
+ const snapshot = execFileSync('ps', ['-axo', 'pid,ppid,pgid,stat,etime,time,comm'], {
181
+ encoding: 'utf8', timeout: 1000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
182
+ });
183
+ writeFileSync(path.join(runDirectory, 'deadline-processes.txt'), snapshot);
184
+ } catch (error) {
185
+ writeFileSync(path.join(runDirectory, 'deadline-processes.txt'), `unavailable: ${error.code || error.message}\n`);
186
+ }
141
187
  try {
142
188
  process.kill(-child.pid, 'SIGKILL'); // kill the whole tree
143
189
  } catch {
@@ -157,13 +203,21 @@ function runStep({ command, args, env }) {
157
203
  stdio: 'inherit',
158
204
  }, STEP_TIMEOUT_MS);
159
205
 
160
- child.on('exit', (code) => {
206
+ child.on('exit', (code, signal) => {
207
+ run.phaseResult = { pid: child.pid, nativeExitCode: code, signal,
208
+ terminationReason: child.ukitTimedOut ? 'phase-deadline' : signal ? 'signal' : 'native-exit',
209
+ elapsedMs: Date.now() - Date.parse(run.phaseStartedAt) };
210
+ persistRun();
161
211
  if (child.ukitTimedOut) {
162
- console.error(`[release:verify] step timed out after ${STEP_TIMEOUT_MS}ms — child killed (check for a wedged ${command}/network)`);
212
+ console.error(`[release:verify] step timed out after ${STEP_TIMEOUT_MS}ms — child killed; phase progress: ${env.UKIT_VERIFY_PROGRESS_FILE}`);
163
213
  }
164
- resolve(code ?? 1);
214
+ resolve(child.ukitTimedOut ? 1 : (code ?? 1));
215
+ });
216
+ child.on('error', (error) => {
217
+ run.phaseResult = { pid: child.pid ?? null, nativeExitCode: null, terminationReason: 'spawn-error', error: error.message };
218
+ persistRun();
219
+ resolve(1);
165
220
  });
166
- child.on('error', () => resolve(1));
167
221
  });
168
222
  }
169
223
 
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { selectManifestItems } from '../manifest/selectItems.js';
4
4
  import { renderTemplateString } from '../render/renderTemplate.js';
5
5
  import { UKIT_PACKAGE_NAME } from './update.js';
6
+ import { validateModuleClosureSync } from '../../scripts/install/module-dependency-closure.mjs';
6
7
 
7
8
  const BINARY_TEMPLATE_EXTENSIONS = new Set([
8
9
  '.ttf',
@@ -325,6 +326,7 @@ export async function buildInstallPlan({
325
326
  }
326
327
  }
327
328
 
329
+ validateModuleClosureSync(planEntries);
328
330
  await downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot });
329
331
 
330
332
  return {