session-orchestrator 3.21.0 → 3.22.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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/040-discovery.mdc +6 -6
  6. package/.cursor/rules/050-plan.mdc +8 -8
  7. package/CHANGELOG.md +101 -0
  8. package/README.md +10 -10
  9. package/agents/memory-proposal-collector.md +6 -4
  10. package/commands/eli5.md +33 -0
  11. package/commands/release.md +5 -3
  12. package/commands/test.md +2 -2
  13. package/docs/components.md +6 -5
  14. package/docs/scope-collision-guard.md +3 -3
  15. package/docs/session-config-reference.md +31 -8
  16. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  17. package/hooks/hooks-codex.json +1 -1
  18. package/hooks/hooks.json +11 -1
  19. package/hooks/on-session-end.mjs +24 -92
  20. package/hooks/on-session-start.mjs +195 -104
  21. package/hooks/pre-auq-clarity.mjs +787 -0
  22. package/hooks/pre-bash-issue-budget.mjs +17 -18
  23. package/package.json +3 -1
  24. package/pi/prompts/eli5.md +12 -0
  25. package/scripts/auq-audit.mjs +825 -0
  26. package/scripts/autopilot.mjs +7 -8
  27. package/scripts/lib/auq/clarity.mjs +1314 -0
  28. package/scripts/lib/auq/parse.mjs +1006 -0
  29. package/scripts/lib/auq/schema.mjs +1457 -0
  30. package/scripts/lib/ci-status-banner.mjs +63 -57
  31. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  32. package/scripts/lib/config/vault-integration.mjs +12 -1
  33. package/scripts/lib/dispatcher/rank.mjs +4 -7
  34. package/scripts/lib/gates/gate-full.mjs +3 -3
  35. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  36. package/scripts/lib/io.mjs +239 -0
  37. package/scripts/lib/issue-budget.mjs +63 -9
  38. package/scripts/lib/owner-interview.mjs +78 -32
  39. package/scripts/lib/peer-discovery.mjs +73 -22
  40. package/scripts/lib/project-hygiene.mjs +64 -4
  41. package/scripts/lib/reconcile/renderer.mjs +17 -4
  42. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  43. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  44. package/scripts/lib/resource-probe.mjs +18 -2
  45. package/scripts/lib/spiral-carryover.mjs +23 -2
  46. package/scripts/lib/state-md/mission-status.mjs +147 -50
  47. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  48. package/scripts/lib/validate/check-hooks-symmetry.mjs +30 -0
  49. package/scripts/lib/validate/check-rules.mjs +153 -9
  50. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  51. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  52. package/scripts/lib/vcs-repo-spec.mjs +182 -13
  53. package/scripts/lib/wave-resource-gate.mjs +67 -73
  54. package/scripts/materialize-wave-scope.mjs +281 -0
  55. package/scripts/release.mjs +443 -122
  56. package/scripts/run-quality-gate.mjs +14 -0
  57. package/scripts/validate-plugin.mjs +3 -0
  58. package/scripts/validate-wave-scope.mjs +6 -1
  59. package/scripts/vault-backfill.mjs +32 -5
  60. package/skills/_shared/parallel-aware-auq.md +30 -24
  61. package/skills/_shared/parallel-aware-preamble.md +31 -2
  62. package/skills/_shared/state-ownership.md +32 -6
  63. package/skills/bootstrap/SKILL.md +2 -1
  64. package/skills/brainstorm/SKILL.md +18 -18
  65. package/skills/brainstorm/soul.md +12 -0
  66. package/skills/discovery/SKILL.md +28 -24
  67. package/skills/eli5/SKILL.md +43 -0
  68. package/skills/evolve/SKILL.md +8 -9
  69. package/skills/gitlab-ops/SKILL.md +30 -26
  70. package/skills/grill/SKILL.md +6 -6
  71. package/skills/grill/soul.md +16 -0
  72. package/skills/memory-cleanup/SKILL.md +2 -2
  73. package/skills/npm-publish/SKILL.md +4 -4
  74. package/skills/peekaboo-driver/SKILL.md +3 -3
  75. package/skills/plan/SKILL.md +18 -16
  76. package/skills/plan/mode-feature.md +1 -1
  77. package/skills/plan/mode-new.md +35 -23
  78. package/skills/plan/soul.md +12 -0
  79. package/skills/reconcile/SKILL.md +3 -3
  80. package/skills/session-end/SKILL.md +53 -20
  81. package/skills/session-end/phase-3-6-tail.md +37 -2
  82. package/skills/session-start/SKILL.md +69 -35
  83. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  84. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  85. package/skills/session-start/soul.md +110 -0
  86. package/skills/test-runner/SKILL.md +2 -2
  87. package/skills/using-orchestrator/SKILL.md +1 -1
  88. package/skills/wave-executor/wave-loop.md +27 -5
  89. package/skills/write-executable-plan/SKILL.md +6 -6
  90. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  91. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  92. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  93. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  94. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  95. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  96. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  97. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  98. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  99. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  100. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  101. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  102. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  103. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  104. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  105. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  106. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  107. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  108. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  109. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  110. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  111. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  112. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  113. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  114. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  115. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  116. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  117. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
@@ -24,13 +24,17 @@
24
24
  // (local, origin, github), github/main mirror parity,
25
25
  // npm registry collision, npm token liveness, CI green
26
26
  // on HEAD, leakage gate over `npm pack --dry-run`.
27
- // --publish Runs --check first, then: token publish via temp
28
- // userconfig (NPM_TOKEN from .env.local), registry
29
- // verify, annotated tag AFTER successful publish
27
+ // --publish Runs --check first, then token publish via temp
28
+ // userconfig (NPM_TOKEN from .env.local). A
29
+ // target-confirmed npm receipt is the irreversible
30
+ // boundary: before it, failure aborts normally; after it,
31
+ // never rerun --publish. The tail tags AFTER receipt
30
32
  // (never before — eliminates "tagged but unpublished"),
31
- // push main + tag to origin AND the github mirror,
32
- // the GitHub release (idempotent, `--verify-tag`), the
33
- // live-site poll, then the post-release checklist.
33
+ // pushes main + tag to origin AND github, handles the
34
+ // GitHub release (`--verify-tag`), reconciles registry
35
+ // propagation, and polls the live site. A tag/push
36
+ // failure skips the tag-dependent GitHub-release and
37
+ // site phases and returns reconciliation guidance.
34
38
  //
35
39
  // USAGE:
36
40
  // node scripts/release.mjs --check [--json] [--skip-ci]
@@ -39,10 +43,11 @@
39
43
  //
40
44
  // EXIT CODES:
41
45
  // 0 success
42
- // 1 check failure (stale surface, missing CHANGELOG entry, tag/registry
43
- // collision, mirror behind, dead token, CI not green, leakage-gate hit)
44
- // 2 system/usage error (git/npm spawn failure, missing NPM_TOKEN,
45
- // unknown flag, --skip-ci combined with --publish)
46
+ // 1 preflight/check failure (stale surface, missing CHANGELOG entry,
47
+ // tag/registry collision, mirror behind, dead token, CI not green,
48
+ // leakage-gate hit) OR post-publish reconciliation required
49
+ // 2 system/usage error before the npm receipt (git/npm spawn failure,
50
+ // missing NPM_TOKEN, unknown flag, --skip-ci combined with --publish)
46
51
  //
47
52
  // FAIL-CLOSED IS THE HOUSE RULE (the defect class this file kept re-growing):
48
53
  // A preflight check reports on evidence it GATHERED. When the gathering
@@ -293,23 +298,37 @@ export function checkChangelogEntry(text, target) {
293
298
  return { ok: problems.length === 0, problems };
294
299
  }
295
300
 
296
- // The seven leakage patterns from skills/npm-publish/SKILL.md, applied to
297
- // `npm pack --dry-run` output lines. Any hit blocks the publish.
301
+ // The leak checks operate only on paths extracted from real packed-entry lines,
302
+ // never on arbitrary `npm notice` prose. `tests` and `.claude` are exact path
303
+ // segments: nested copies leak too, while `contest` and `.claude-plugin` do not.
304
+ function hasPathSegment(path, segment) {
305
+ return path.split('/').includes(segment);
306
+ }
307
+
298
308
  export const LEAKAGE_PATTERNS = [
299
- { name: 'tests/', re: /npm notice.* tests\// },
300
- { name: '.orchestrator/', re: /npm notice.*\.orchestrator\// },
301
- { name: '.claude/', re: /npm notice.*\s\.claude\// },
302
- { name: '.github/', re: /npm notice.*\.github\// },
303
- { name: 'node_modules', re: /node_modules/ },
304
- { name: '.env', re: /npm notice.*\.env/i },
305
- { name: 'owner.yaml', re: /owner\.yaml/i },
309
+ { name: 'tests/', matches: (path) => hasPathSegment(path, 'tests') },
310
+ { name: '.orchestrator/', matches: (path) => /\.orchestrator\//.test(path) },
311
+ { name: '.claude/', matches: (path) => hasPathSegment(path, '.claude') },
312
+ { name: '.github/', matches: (path) => /\.github\//.test(path) },
313
+ { name: 'node_modules', matches: (path) => /node_modules/.test(path) },
314
+ { name: '.env', matches: (path) => /\.env/i.test(path) },
315
+ { name: 'owner.yaml', matches: (path) => /owner\.yaml/i.test(path) },
306
316
  // Claimed as checked by docs/distribution/npm-publish-checklist.md long before
307
317
  // any code checked it (measured 2026-08-19: 3 leakage lists, 3 different sets).
308
318
  // `files` in package.json overrides .gitignore, so a stray .DS_Store inside a
309
319
  // shipped directory reaches the tarball.
310
- { name: '.DS_Store', re: /\.DS_Store/ },
320
+ { name: '.DS_Store', matches: (path) => /\.DS_Store/.test(path) },
311
321
  ];
312
322
 
323
+ // Bootstrap's public Standard path copies each selected template in full, so
324
+ // these two sanity tests are intentional scaffold assets, not package-internal
325
+ // test material. This is exact by path and applies only to the `tests/` class:
326
+ // do not turn it into a templates/** or segment-level bypass.
327
+ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
328
+ 'templates/node-minimal/tests/sanity.test.ts',
329
+ 'templates/python-uv/tests/test_sanity.py',
330
+ ]);
331
+
313
332
  // `commands/release.md` quotes the `npm view` OUTPUT that proves the 3.18.0 gap,
314
333
  // dated at the line. Bumping it would destroy the evidence it exists to carry —
315
334
  // the registry state on that date is the whole point of the paragraph.
@@ -324,12 +343,15 @@ export const LEAKAGE_PATTERNS = [
324
343
  // exempts exactly the lines marked `site-numbers:historical`.
325
344
  export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|docs\/|tests\/|skills\/npm-publish\/|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|commands\/release\.md)/;
326
345
 
327
- /** Pure check over pack-output lines. Returns violations: {name, line}[]. */
346
+ /** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
328
347
  export function checkLeakage(lines) {
329
348
  const violations = [];
330
349
  for (const line of lines) {
331
- for (const { name, re } of LEAKAGE_PATTERNS) {
332
- if (re.test(line)) violations.push({ name, line: line.trim() });
350
+ const entry = parsePackedEntry(line);
351
+ if (!entry) continue;
352
+ for (const { name, matches } of LEAKAGE_PATTERNS) {
353
+ if (name === 'tests/' && INTENTIONAL_TEST_ASSET_PATHS.has(entry.path)) continue;
354
+ if (matches(entry.path)) violations.push({ name, line: line.trim() });
333
355
  }
334
356
  }
335
357
  return violations;
@@ -339,27 +361,47 @@ export function checkLeakage(lines) {
339
361
  * One packed tarball entry in `npm pack --dry-run` output:
340
362
  * `npm notice 1.3kB .claude-plugin/marketplace.json`.
341
363
  *
342
- * The previous inline counter was `/npm notice.*[0-9]+B /`, which requires a
343
- * DIGIT immediately before the `B` and therefore matched only entries sized in
344
- * plain bytes 108 of the 830 real entries at 8984224. It was never load-
345
- * bearing (it only decorated a detail string), but it becomes load-bearing the
346
- * moment a floor is asserted on it, so it is fixed here rather than floored at
347
- * a number that means nothing.
364
+ * This grammar intentionally excludes npm's package metadata and summary
365
+ * notices. Leakage decisions must be made over a file path, not a sentence
366
+ * that happens to mention one.
367
+ */
368
+ export const PACKED_ENTRY_RE = /^npm notice\s+(\d+(?:\.\d+)?\s*(?:B|kB|MB|GB))\s+(\S.*)$/;
369
+
370
+ /**
371
+ * Parse an npm packed-entry notice into its path. Returns null for every other
372
+ * npm notice line, including package metadata and summaries.
373
+ *
374
+ * @param {string} line
375
+ * @returns {{path: string}|null}
348
376
  */
349
- export const PACKED_ENTRY_RE = /^npm notice\s+[\d.]+\s*(?:B|kB|MB|GB)\s+\S/;
377
+ export function parsePackedEntry(line) {
378
+ const match = line.match(PACKED_ENTRY_RE);
379
+ return match ? { path: match[2].trim() } : null;
380
+ }
350
381
 
351
382
  /**
352
383
  * Floor on parsed packed entries, below which the leak scan is presumed BLIND
353
384
  * rather than clean.
354
385
  *
355
- * Measured at 8984224 with `npm pack --dry-run`: npm's own summary reports
356
- * `total files: 830` and {@link PACKED_ENTRY_RE} independently counts 830
386
+ * Measured 2026-08-21 with `npm pack --dry-run`: npm's own summary reports
387
+ * `total files: 805` and {@link PACKED_ENTRY_RE} independently counts 805
357
388
  * two differently-shaped measurements agreeing. Package size 2.9 MB, unpacked
358
- * 8.9 MB. (docs/distribution/npm-publish-checklist.md still records the older
359
- * "~750 files, ~6.5 MB unpacked" baseline; that file has a different owner.)
389
+ * 9.0 MB. The count dropped from 830 when `package.json` `files` gained
390
+ * `!scripts/tests/**` and `!skills/vault-sync/tests/**`; those 28 entries were
391
+ * shipped in 3.21.0 (scanned: no secrets, no owner data — ballast, not an
392
+ * incident). Independently confirmable after the fact:
393
+ * `npm view session-orchestrator@3.21.0 dist.fileCount` returns 832 — npm's own
394
+ * count of what the registry accepted for THAT version, from outside this repo,
395
+ * and therefore still the pre-exclusion number.
360
396
  *
361
- * 400 is a FLOOR, not a pin deliberately ~48% of today's count. It cannot
362
- * break on growth (the pack only grows), and it is far enough below 830 that a
397
+ * (An earlier revision of this comment claimed the checklist "still records the
398
+ * older ~750 files" baseline. It did not: commit a2e495c rewrote that line to
399
+ * the measured 830/2.9/8.9 at the same SHA this comment was written. The claim
400
+ * was a second copy of a fact, contradicting the first, inside the file that
401
+ * argues against second copies. Caught by the post-publish review panel.)
402
+ *
403
+ * 400 is a FLOOR, not a pin — deliberately ~50% of today's count. It cannot
404
+ * break on growth (the pack only grows), and it is far enough below 805 that a
363
405
  * deliberate docs/skills prune would not trip it. What it does catch is the
364
406
  * whole failure class in one number: an npm output-format change, an
365
407
  * `npm notice` prefix rename, a `files`/`.npmignore` edit that drops entire
@@ -481,7 +523,7 @@ export function evaluateLeakageGate(pack, { minEntries = MIN_PACKED_ENTRIES } =
481
523
  return { ok: false, detail: `npm pack failed (exit ${pack.status}): ${(pack.stderr || '').trim().slice(-200)}` };
482
524
  }
483
525
  const lines = `${pack.stdout || ''}\n${pack.stderr || ''}`.split('\n');
484
- const entries = lines.filter((l) => PACKED_ENTRY_RE.test(l)).length;
526
+ const entries = lines.filter(parsePackedEntry).length;
485
527
  if (entries < minEntries) {
486
528
  return {
487
529
  ok: false,
@@ -619,8 +661,14 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
619
661
  // FAILED status call is indistinguishable from a genuinely clean tree, and
620
662
  // the empty-reads-as-all-clear shape is exactly the fail-open this file was
621
663
  // hardened against elsewhere. Same reasoning for the two `git tag -l` reads
622
- // below those are the only other preflight subprocesses whose emptiness
623
- // means "all clear" (census: all 14 `run(` call sites in this file).
664
+ // below. A third subprocess shares the shape the `git ls-remote --tags`
665
+ // collision probe further down reads an empty stdout as "no collision" — but
666
+ // that one guards it with an explicit `ls.status === 0`, so it is not
667
+ // fail-open. Emptiness-means-all-clear is the shape to look for; reading the
668
+ // status is what makes it safe. (Census: 14 `run(` call sites in this file.
669
+ // It does NOT cover the raw `spawnSync` calls — a payload-keyed census misses
670
+ // the consumer that uses a different channel, which is how the unchecked
671
+ // propagation wait stayed invisible to it.)
624
672
  const status = run('git', ['status', '--porcelain'], { cwd: repoRoot });
625
673
  const dirty = (status.stdout || '').trim();
626
674
  add(
@@ -726,7 +774,18 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
726
774
  }
727
775
 
728
776
  // 7. Leakage gate over the actual pack file list.
729
- const pack = run('npm', ['pack', '--dry-run'], { cwd: repoRoot });
777
+ // `npm_config_loglevel` is INHERITED, and `npm pack --dry-run` writes its whole
778
+ // file listing as `npm notice` lines. Any ancestor that ran under `npm run
779
+ // --silent` (the pre-push gate does exactly that) therefore hands this child a
780
+ // silent loglevel and the listing is EMPTY with exit 0 — measured: 818 notice
781
+ // lines normally, 0 under `npm_config_loglevel=silent`. The blind-scan floor
782
+ // turns that into a correct fail-closed verdict, but a red leakage row that
783
+ // means "we could not look" is not the row anyone reads it as. Pin the level
784
+ // rather than inherit it, so the gate's input never depends on its caller.
785
+ const pack = run('npm', ['pack', '--dry-run'], {
786
+ cwd: repoRoot,
787
+ env: { ...process.env, npm_config_loglevel: 'notice' },
788
+ });
730
789
  const leakage = evaluateLeakageGate(pack);
731
790
  add('leakage-gate', leakage.ok, leakage.detail);
732
791
 
@@ -770,49 +829,336 @@ function withTempUserconfig(token, fn) {
770
829
  chmodSync(tmpRc, 0o600);
771
830
  return fn(tmpRc);
772
831
  } finally {
773
- rmSync(tmpDir, { recursive: true, force: true });
832
+ // NEVER let cleanup decide the release outcome. This finally runs AFTER
833
+ // `npm publish` has already published and BEFORE the caller evaluates the
834
+ // receipt, so a throwing rmSync (EPERM/EBUSY -- `force` only swallows
835
+ // ENOENT) surfaced as a pre-receipt system failure: published, untagged,
836
+ // unpushed, and reported as "safe to re-run". That is the #1088 F1 shape at
837
+ // its last remaining site. A surviving 0600 token file is a hygiene problem,
838
+ // so it is announced rather than swallowed.
839
+ try {
840
+ rmSync(tmpDir, { recursive: true, force: true });
841
+ } catch (err) {
842
+ process.stderr.write(
843
+ `WARN: could not remove temporary npm userconfig ${tmpDir} (${err?.message ?? err}). ` +
844
+ `It contains a write token — delete it and rotate the token.\n`,
845
+ );
846
+ }
847
+ }
848
+ }
849
+
850
+ /**
851
+ * Evaluate npm publish output for the receipt that makes the release immutable.
852
+ * A successful process alone is insufficient: the receipt must name this package
853
+ * and this exact target version.
854
+ *
855
+ * @param {{status: number|null, stdout?: string, stderr?: string}} result
856
+ * @param {string} target
857
+ * @returns {{confirmed: boolean, target: string, detail: string}}
858
+ */
859
+ export function evaluatePublishReceipt(result, target) {
860
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
861
+ const receipt = new RegExp(`(?:^|\\n)\\+ ${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@${target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\r?$|\\s)`);
862
+ const confirmed = result.status === 0 && receipt.test(output);
863
+ return {
864
+ confirmed,
865
+ target,
866
+ detail: confirmed
867
+ ? `${PACKAGE_NAME}@${target} receipt confirmed`
868
+ : `npm publish did not emit a target-confirmed receipt for ${PACKAGE_NAME}@${target} (exit ${result.status})`,
869
+ };
870
+ }
871
+
872
+ /**
873
+ * Poll the registry after npm has issued a target-confirmed receipt. Every
874
+ * failure remains visible, but none invalidates the already irreversible npm
875
+ * publish; callers must reconcile rather than retry `--publish`.
876
+ *
877
+ * @param {string} repoRoot
878
+ * @param {string} target
879
+ * @param {{attempts?: number, delaySeconds?: number, runImpl?: Function, waitImpl?: Function}} [deps]
880
+ * @returns {{ok: boolean, kind: 'verified'|'timeout'|'query-failed'|'wait-failed', attempts: number, detail: string}}
881
+ */
882
+ export function waitForRegistryPropagation(repoRoot, target, deps = {}) {
883
+ const attempts = deps.attempts ?? 5;
884
+ const delaySeconds = deps.delaySeconds ?? 3;
885
+ const runImpl = deps.runImpl ?? run;
886
+ const waitImpl = deps.waitImpl ?? (() => runImpl('sleep', [String(delaySeconds)], { cwd: repoRoot }));
887
+
888
+ for (let attempt = 1; attempt <= attempts; attempt++) {
889
+ let view;
890
+ try {
891
+ view = runImpl('npm', ['view', PACKAGE_NAME, 'version'], { cwd: repoRoot });
892
+ } catch (err) {
893
+ return {
894
+ ok: false,
895
+ kind: 'query-failed',
896
+ attempts: attempt,
897
+ detail: `registry query failed on attempt ${attempt}/${attempts}: ${err.message}`,
898
+ };
899
+ }
900
+ if (view.status === 0 && (view.stdout || '').trim() === target) {
901
+ return {
902
+ ok: true,
903
+ kind: 'verified',
904
+ attempts: attempt,
905
+ detail: `registry reports ${target} on attempt ${attempt}/${attempts}`,
906
+ };
907
+ }
908
+ if (view.status !== 0) {
909
+ return {
910
+ ok: false,
911
+ kind: 'query-failed',
912
+ attempts: attempt,
913
+ detail: `registry query failed on attempt ${attempt}/${attempts} (exit ${view.status}): ${(view.stderr || view.stdout || '').trim().slice(0, 300)}`,
914
+ };
915
+ }
916
+ if (attempt < attempts) {
917
+ let wait;
918
+ try {
919
+ wait = waitImpl({ attempt, delaySeconds });
920
+ } catch (err) {
921
+ return {
922
+ ok: false,
923
+ kind: 'wait-failed',
924
+ attempts: attempt,
925
+ detail: `registry propagation wait failed after attempt ${attempt}/${attempts}: ${err.message}`,
926
+ };
927
+ }
928
+ if (!wait || wait.status !== 0 || wait.error) {
929
+ return {
930
+ ok: false,
931
+ kind: 'wait-failed',
932
+ attempts: attempt,
933
+ detail: `registry propagation wait failed after attempt ${attempt}/${attempts} (exit ${wait?.status ?? 'unknown'}): ${(wait?.error?.message || wait?.stderr || wait?.stdout || '').trim().slice(0, 300)}`,
934
+ };
935
+ }
936
+ }
774
937
  }
938
+ return {
939
+ ok: false,
940
+ kind: 'timeout',
941
+ attempts,
942
+ detail: `registry did not report ${target} after ${attempts} attempts`,
943
+ };
775
944
  }
776
945
 
777
- function publish(repoRoot, target) {
946
+ /**
947
+ * Publish and return the receipt boundary plus the registry reconciliation
948
+ * result. Pre-receipt failures throw; post-receipt propagation failures return.
949
+ *
950
+ * @param {string} repoRoot
951
+ * @param {string} target
952
+ * @param {{runImpl?: Function, waitImpl?: Function, attempts?: number, delaySeconds?: number}} [deps]
953
+ * @returns {{receipt: {confirmed: boolean, target: string, detail: string}, propagation: ReturnType<typeof waitForRegistryPropagation>}}
954
+ */
955
+ // Deliberately NOT exported: this is the irreversible act, and every production
956
+ // path to it runs through main() -> preflight() (leakage gate, CI gate, dirty-tree
957
+ // gate). Exporting it made the whole gate chain bypassable by any importer, and no
958
+ // consumer needs it -- the tests drive runPublishRelease with an injected publisher.
959
+ function publish(repoRoot, target, deps = {}) {
778
960
  const token = loadNpmToken(repoRoot);
961
+ const runImpl = deps.runImpl ?? run;
779
962
  const res = withTempUserconfig(token, (tmpRc) =>
780
- run('npm', ['publish', '--access', 'public', '--userconfig', tmpRc], { cwd: repoRoot }),
963
+ runImpl('npm', ['publish', '--access', 'public', '--userconfig', tmpRc], { cwd: repoRoot }),
781
964
  );
782
- const out = `${res.stdout}\n${res.stderr}`;
783
- if (res.status !== 0 || !out.includes(`+ ${PACKAGE_NAME}@${target}`)) {
784
- // Never echo the raw output wholesale into logs beyond the error slice —
785
- // it cannot contain the token (npm masks userconfig), but stay frugal.
786
- throw new Error(`npm publish failed (exit ${res.status}): ${out.slice(0, 800)}`);
787
- }
788
-
789
- // Registry verify with propagation retries.
790
- for (let attempt = 1; attempt <= 5; attempt++) {
791
- const view = run('npm', ['view', PACKAGE_NAME, 'version'], { cwd: repoRoot });
792
- if (view.status === 0 && view.stdout.trim() === target) return;
793
- if (attempt < 5) spawnSync('sleep', ['3']);
794
- }
795
- throw new Error(`registry verify failed: npm view does not report ${target} after 5 attempts`);
965
+ const receipt = evaluatePublishReceipt(res, target);
966
+ if (!receipt.confirmed) throw new Error(receipt.detail);
967
+
968
+ const propagation = waitForRegistryPropagation(repoRoot, target, {
969
+ attempts: deps.attempts,
970
+ delaySeconds: deps.delaySeconds,
971
+ runImpl,
972
+ waitImpl: deps.waitImpl,
973
+ });
974
+ return { receipt, propagation };
796
975
  }
797
976
 
798
977
  function tagAndPush(repoRoot, target) {
799
978
  const tag = `v${target}`;
800
- const excerpt = changelogExcerpt(repoRoot, target);
801
- const msgDir = mkdtempSync(join(tmpdir(), 'release-tagmsg-'));
802
- const msgFile = join(msgDir, 'msg');
979
+ const progress = { tag, localTagCreated: false, pushed: [], remotes: [] };
803
980
  try {
804
- writeFileSync(msgFile, `${tag}\n\n${excerpt}\n`);
805
- mustRun('git', ['tag', '-a', tag, '-F', msgFile], { cwd: repoRoot });
806
- } finally {
807
- rmSync(msgDir, { recursive: true, force: true });
981
+ const excerpt = changelogExcerpt(repoRoot, target);
982
+ const msgDir = mkdtempSync(join(tmpdir(), 'release-tagmsg-'));
983
+ const msgFile = join(msgDir, 'msg');
984
+ try {
985
+ writeFileSync(msgFile, `${tag}\n\n${excerpt}\n`);
986
+ mustRun('git', ['tag', '-a', tag, '-F', msgFile], { cwd: repoRoot });
987
+ progress.localTagCreated = true;
988
+ } finally {
989
+ rmSync(msgDir, { recursive: true, force: true });
990
+ }
991
+ for (const remote of ['origin', 'github']) {
992
+ const remoteProgress = { remote, mainPushed: false, tagPushed: false };
993
+ progress.remotes.push(remoteProgress);
994
+ mustRun('git', ['push', remote, 'main'], { cwd: repoRoot });
995
+ remoteProgress.mainPushed = true;
996
+ mustRun('git', ['push', remote, tag], { cwd: repoRoot });
997
+ remoteProgress.tagPushed = true;
998
+ progress.pushed.push(remote);
999
+ }
1000
+ return { tag, pushed: progress.pushed };
1001
+ } catch (err) {
1002
+ const failure = err instanceof Error ? err : new Error(String(err));
1003
+ failure.releaseProgress = progress;
1004
+ throw failure;
808
1005
  }
809
- const pushed = [];
810
- for (const remote of ['origin', 'github']) {
811
- mustRun('git', ['push', remote, 'main'], { cwd: repoRoot });
812
- mustRun('git', ['push', remote, tag], { cwd: repoRoot });
813
- pushed.push(remote);
1006
+ }
1007
+
1008
+ /**
1009
+ * Run the irreversible-release tail after npm's target-confirmed receipt.
1010
+ * A tag/push failure stops tag-dependent phases; every other post-receipt
1011
+ * finding remains a returned reconciliation result rather than a retry signal.
1012
+ *
1013
+ * @param {string} repoRoot
1014
+ * @param {string} target
1015
+ * @param {{publishImpl?: Function, tagAndPushImpl?: Function, ensureGithubReleaseImpl?: Function, verifyLiveSiteImpl?: Function}} [deps]
1016
+ * @returns {Promise<{status: 'complete'|'post-publish-reconciliation', receipt: object, tag: string|null, pushed: string[], tagProgress?: object, release: object, live: object, propagation: object, reconciliation: Array<{phase: string, kind?: string, detail: string}>}>}
1017
+ */
1018
+ export async function runPublishRelease(repoRoot, target, deps = {}) {
1019
+ // Fail-closed: the real publisher must be handed in explicitly. Defaulting to
1020
+ // the live `npm publish --access public` meant an importer that merely forgot
1021
+ // `publishImpl` performed an irreversible public release with the token from
1022
+ // .env.local and no preflight. main() wires it at the one call site that sits
1023
+ // behind the gate chain.
1024
+ const publishImpl = deps.publishImpl;
1025
+ if (typeof publishImpl !== 'function') {
1026
+ throw new Error('runPublishRelease requires an explicit publishImpl — refusing to publish by default');
1027
+ }
1028
+ const tagAndPushImpl = deps.tagAndPushImpl ?? tagAndPush;
1029
+ const ensureGithubReleaseImpl = deps.ensureGithubReleaseImpl ?? ensureGithubRelease;
1030
+ const verifyLiveSiteImpl = deps.verifyLiveSiteImpl ?? verifyLiveSite;
1031
+ const publication = publishImpl(repoRoot, target);
1032
+
1033
+ if (!publication?.receipt?.confirmed || publication.receipt.target !== target) {
1034
+ throw new Error(`refusing release tail without a target-confirmed npm publish receipt for ${PACKAGE_NAME}@${target}`);
814
1035
  }
815
- return { tag, pushed };
1036
+
1037
+ const propagation = publication.propagation;
1038
+ let tagAndPushResult;
1039
+ try {
1040
+ tagAndPushResult = tagAndPushImpl(repoRoot, target);
1041
+ } catch (err) {
1042
+ const rawProgress = err?.releaseProgress;
1043
+ const remotes = Array.isArray(rawProgress?.remotes)
1044
+ ? rawProgress.remotes
1045
+ .filter((remote) => typeof remote?.remote === 'string')
1046
+ .map((remote) => ({
1047
+ remote: remote.remote,
1048
+ mainPushed: remote.mainPushed === true,
1049
+ tagPushed: remote.tagPushed === true,
1050
+ }))
1051
+ : [];
1052
+ const tagProgress = {
1053
+ tag: typeof rawProgress?.tag === 'string' ? rawProgress.tag : null,
1054
+ localTagCreated: rawProgress?.localTagCreated === true,
1055
+ remotes,
1056
+ };
1057
+ const pushed = remotes.filter((remote) => remote.mainPushed && remote.tagPushed).map((remote) => remote.remote);
1058
+ const prerequisite = 'skipped because tag-and-push did not complete';
1059
+ return {
1060
+ status: 'post-publish-reconciliation',
1061
+ receipt: publication.receipt,
1062
+ tag: tagProgress.tag,
1063
+ pushed,
1064
+ tagProgress,
1065
+ release: { ok: false, skipped: true, state: 'skipped-prerequisite', detail: `GitHub release ${prerequisite}` },
1066
+ live: { ok: false, skipped: true, state: 'skipped-prerequisite', detail: `live-site verification ${prerequisite}` },
1067
+ propagation,
1068
+ reconciliation: [
1069
+ { phase: 'tag-and-push', kind: 'failed', detail: err instanceof Error ? err.message : String(err) },
1070
+ { phase: 'github-release', kind: 'skipped-prerequisite', detail: `GitHub release ${prerequisite}` },
1071
+ { phase: 'live-site', kind: 'skipped-prerequisite', detail: `live-site verification ${prerequisite}` },
1072
+ ],
1073
+ };
1074
+ }
1075
+
1076
+ const { tag, pushed } = tagAndPushResult;
1077
+ const release = ensureGithubReleaseImpl(repoRoot, target);
1078
+ const live = await verifyLiveSiteImpl(target);
1079
+ const reconciliation = [];
1080
+ if (!propagation?.ok) {
1081
+ reconciliation.push({
1082
+ phase: 'registry-propagation',
1083
+ kind: propagation?.kind ?? 'unknown',
1084
+ detail: propagation?.detail ?? 'registry propagation was not verified',
1085
+ });
1086
+ }
1087
+ if (!release.ok) reconciliation.push({ phase: 'github-release', detail: release.detail });
1088
+ if (!live.ok) reconciliation.push({ phase: 'live-site', detail: live.detail });
1089
+
1090
+ return {
1091
+ status: reconciliation.length === 0 ? 'complete' : 'post-publish-reconciliation',
1092
+ receipt: publication.receipt,
1093
+ tag,
1094
+ pushed,
1095
+ release,
1096
+ live,
1097
+ propagation,
1098
+ reconciliation,
1099
+ };
1100
+ }
1101
+
1102
+ /**
1103
+ * Print a completed publish-tail outcome and return the CLI exit code.
1104
+ *
1105
+ * @param {{status: string, propagation: object, tag: string|null, pushed: string[], release: object, live: object, reconciliation: Array<{phase: string, kind?: string, detail: string}>}} outcome
1106
+ * @param {string} target
1107
+ * @param {{log?: Function, error?: Function}} [io]
1108
+ * @returns {number}
1109
+ */
1110
+ export function printPublishOutcome(outcome, target, io = {}) {
1111
+ const log = io.log ?? console.log;
1112
+ const error = io.error ?? console.error;
1113
+ const tagAndPushFailed = outcome.reconciliation.some((item) => item.phase === 'tag-and-push');
1114
+
1115
+ log(` + ${PACKAGE_NAME}@${target} — target-confirmed npm receipt.`);
1116
+ if (outcome.propagation.ok) {
1117
+ log(` registry verified (${outcome.propagation.detail}).`);
1118
+ } else {
1119
+ error(`\nRECONCILIATION: registry propagation is not yet verified — ${outcome.propagation.detail}`);
1120
+ }
1121
+ if (!tagAndPushFailed) {
1122
+ log(` tagged ${outcome.tag} (AFTER publish) and pushed main+tag to: ${outcome.pushed.join(', ')}.`);
1123
+ }
1124
+
1125
+ if (outcome.release.skipped) {
1126
+ error(`\nSKIPPED: ${outcome.release.detail}.`);
1127
+ } else if (outcome.release.ok) {
1128
+ log(` ${outcome.release.detail}.`);
1129
+ } else {
1130
+ error(`\nRECONCILIATION: ${outcome.release.detail}`);
1131
+ if (outcome.release.state === 'create-failed') {
1132
+ error(` Recover with: gh release create ${outcome.tag} --verify-tag --title ${outcome.tag} --notes-file <changelog excerpt>`);
1133
+ } else {
1134
+ error(' Inspect `gh release view` and its authentication/network state before attempting any create.');
1135
+ }
1136
+ }
1137
+
1138
+ if (outcome.live.skipped) {
1139
+ error(`\nSKIPPED: ${outcome.live.detail}.`);
1140
+ } else if (!outcome.live.ok) {
1141
+ error(`\nRECONCILIATION: live site did not reach ${target}.`);
1142
+ error(` ${outcome.live.detail}`);
1143
+ error(' Check https://vercel.com/kanevrys-projects/session-orchestrator for the deploy.');
1144
+ } else {
1145
+ log(` site live at ${target} (${outcome.live.detail}).`);
1146
+ }
1147
+
1148
+ if (outcome.status === 'post-publish-reconciliation') {
1149
+ error('\nPost-publish reconciliation required: npm has accepted the target release.');
1150
+ for (const item of outcome.reconciliation) {
1151
+ error(` - ${item.phase}${item.kind ? ` (${item.kind})` : ''}: ${item.detail}`);
1152
+ }
1153
+ error(' Do NOT rerun `--publish`; reconcile the listed post-publish state directly.');
1154
+ return 1;
1155
+ }
1156
+
1157
+ log(`\nRelease complete: ${PACKAGE_NAME}@${target} is published, tagged, released and live.`);
1158
+ log('\nPost-release checklist (manual):');
1159
+ log(' 1. Rotate/delete the npm token: https://www.npmjs.com/settings/<user>/tokens');
1160
+ log(' 2. pi.dev gallery indexes asynchronously — do not block on it.');
1161
+ return 0;
816
1162
  }
817
1163
 
818
1164
  /**
@@ -831,9 +1177,9 @@ function tagAndPush(repoRoot, target) {
831
1177
  * - `--verify-tag` makes gh refuse when the tag is not on the remote, so
832
1178
  * "release without a tag" is structurally impossible rather than merely
833
1179
  * discouraged.
834
- * - The `gh release view` probe first makes a re-run a no-op instead of an
835
- * error, so a second `--publish` pass after a partial failure is not blocked
836
- * by the step that already succeeded.
1180
+ * - The `gh release view` probe avoids a duplicate-release error when a
1181
+ * release is already present. It does not authorize rerunning `--publish`:
1182
+ * post-receipt failures are reconciled directly.
837
1183
  * - The `-R` spec comes from `resolveRepoSpec({vcs:'github'})` (#1039), not a
838
1184
  * hardcoded owner/repo, so a fork or a renamed remote targets its own repo.
839
1185
  *
@@ -844,7 +1190,7 @@ function tagAndPush(repoRoot, target) {
844
1190
  * @param {string} repoRoot
845
1191
  * @param {string} target
846
1192
  * @param {{runImpl?: Function, repoSpec?: string}} [deps] — injection seam for tests
847
- * @returns {{ok: boolean, created: boolean, tag: string, detail: string, argv?: string[]}}
1193
+ * @returns {{ok: boolean, created: boolean, tag: string, state: 'exists'|'created'|'unknown'|'create-failed', detail: string, argv?: string[]}}
848
1194
  */
849
1195
  export function ensureGithubRelease(repoRoot, target, deps = {}) {
850
1196
  const runImpl = deps.runImpl ?? run;
@@ -856,8 +1202,21 @@ export function ensureGithubRelease(repoRoot, target, deps = {}) {
856
1202
 
857
1203
  try {
858
1204
  const existing = runImpl('gh', ['release', 'view', tag, ...repoFlag], { cwd: repoRoot });
859
- if (existing.status === 0) {
860
- return { ok: true, created: false, tag, detail: `GitHub release ${tag} already exists — no-op` };
1205
+ const viewOutput = `${existing.stdout || ''}\n${existing.stderr || ''}`.trim();
1206
+ if (existing.status === 0 && viewOutput) {
1207
+ return { ok: true, created: false, tag, state: 'exists', detail: `GitHub release ${tag} already exists — no-op` };
1208
+ }
1209
+ // `gh release view` is tri-state. Only its documented absence response is
1210
+ // permission to create; auth, network, empty and malformed responses leave
1211
+ // release state unknown and must not trigger a write to GitHub.
1212
+ if (!(existing.status === 1 && /^release not found$/i.test(viewOutput))) {
1213
+ return {
1214
+ ok: false,
1215
+ created: false,
1216
+ tag,
1217
+ state: 'unknown',
1218
+ detail: `could not determine whether GitHub release ${tag} exists (gh release view exited ${existing.status}: ${viewOutput.slice(0, 300) || 'empty output'})`,
1219
+ };
861
1220
  }
862
1221
 
863
1222
  const notesDir = mkdtempSync(join(tmpdir(), 'release-ghnotes-'));
@@ -872,16 +1231,17 @@ export function ensureGithubRelease(repoRoot, target, deps = {}) {
872
1231
  ok: false,
873
1232
  created: false,
874
1233
  tag,
1234
+ state: 'create-failed',
875
1235
  argv,
876
1236
  detail: `gh release create exited ${created.status}: ${(created.stderr || created.stdout || '').trim().slice(0, 300)}`,
877
1237
  };
878
1238
  }
879
- return { ok: true, created: true, tag, argv, detail: `GitHub release ${tag} created (--verify-tag)` };
1239
+ return { ok: true, created: true, tag, state: 'created', argv, detail: `GitHub release ${tag} created (--verify-tag)` };
880
1240
  } finally {
881
1241
  rmSync(notesDir, { recursive: true, force: true });
882
1242
  }
883
1243
  } catch (err) {
884
- return { ok: false, created: false, tag, detail: `gh could not be run: ${err.message}` };
1244
+ return { ok: false, created: false, tag, state: 'unknown', detail: `gh could not be run: ${err.message}` };
885
1245
  }
886
1246
  }
887
1247
 
@@ -962,8 +1322,10 @@ async function main() {
962
1322
  if (values.help) {
963
1323
  console.log('Usage: node scripts/release.mjs [--set-version X.Y.Z | --check | --publish] [--skip-ci] [--json]');
964
1324
  console.log('Release als ein Dispatch: surface sync, preflight checks, token publish, tag AFTER publish.');
1325
+ console.log(' Receipt boundary: before the confirmed npm receipt, failure aborts; after it, never rerun --publish.');
1326
+ console.log(' Tag/push failure after receipt skips GitHub-release and site phases and returns reconciliation guidance.');
965
1327
  console.log(' --skip-ci allowed with --check only; REFUSED with --publish (it verifies nothing).');
966
- console.log('Exit codes: 0 success, 1 check failure, 2 system/usage error.');
1328
+ console.log('Exit codes: 0 success, 1 preflight/check failure or post-publish reconciliation, 2 pre-receipt system/usage error.');
967
1329
  return 0;
968
1330
  }
969
1331
 
@@ -1015,49 +1377,8 @@ async function main() {
1015
1377
  if (!values.publish) return 0;
1016
1378
 
1017
1379
  console.log(`\nPublishing ${PACKAGE_NAME}@${target} ...`);
1018
- publish(repoRoot, target);
1019
- console.log(` + ${PACKAGE_NAME}@${target} — registry verified.`);
1020
- const { tag, pushed } = tagAndPush(repoRoot, target);
1021
- console.log(` tagged ${tag} (AFTER publish) and pushed main+tag to: ${pushed.join(', ')}.`);
1022
-
1023
- // GitHub release — after the tag is on the remote (so --verify-tag can do
1024
- // its job), before the site poll. Not fatal on its own: npm and both tags
1025
- // are already published at this point, and reporting a shipped release as a
1026
- // crash would be a worse lie than reporting the one missing artefact.
1027
- const release = ensureGithubRelease(repoRoot, target);
1028
- if (release.ok) {
1029
- console.log(` ${release.detail}.`);
1030
- } else {
1031
- console.error(`\nFAIL: ${release.detail}`);
1032
- console.error(' npm publish and both tag pushes SUCCEEDED — only the GitHub release is missing.');
1033
- console.error(` Recover with: gh release create ${tag} --verify-tag --title ${tag} --notes-file <changelog excerpt>`);
1034
- }
1035
-
1036
- // The push to `github` above triggers the Vercel git integration, which
1037
- // deploys site/ (see vercel.json `outputDirectory`). The deploy is async,
1038
- // so poll rather than assume. This replaces the old manual checklist line
1039
- // `cd site && vercel --prod` — a checklist line is not a mechanism, and it
1040
- // was skipped twice in four weeks (#1043), leaving the live site a full
1041
- // release behind while every other surface said otherwise.
1042
- const live = await verifyLiveSite(target);
1043
- if (!live.ok) {
1044
- console.error(`\nFAIL: live site did not reach ${target}.`);
1045
- console.error(` ${live.detail}`);
1046
- console.error(' npm and the tags ARE published — only the site lag remains.');
1047
- console.error(' Check https://vercel.com/kanevrys-projects/session-orchestrator for the deploy.');
1048
- } else {
1049
- console.log(` site live at ${target} (${live.detail}).`);
1050
- }
1051
-
1052
- // Both post-publish steps report before either decides the exit code —
1053
- // an operator who lost the GitHub release should still learn whether the
1054
- // site deployed, and vice versa.
1055
- if (!release.ok || !live.ok) return 1;
1056
-
1057
- console.log('\nPost-release checklist (manual):');
1058
- console.log(' 1. Rotate/delete the npm token: https://www.npmjs.com/settings/<user>/tokens');
1059
- console.log(' 2. pi.dev gallery indexes asynchronously — do not block on it.');
1060
- return 0;
1380
+ const outcome = await runPublishRelease(repoRoot, target, { publishImpl: publish });
1381
+ return printPublishOutcome(outcome, target);
1061
1382
  }
1062
1383
 
1063
1384
  console.error('Nothing to do — pass --check, --publish, or --set-version X.Y.Z (see --help).');