@webpresso/agent-kit 3.3.2 → 3.3.4

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 (167) hide show
  1. package/THIRD-PARTY-NOTICES.md +12 -10
  2. package/catalog/AGENTS.md.tpl +3 -5
  3. package/catalog/agent/rules/changeset-release.md +15 -4
  4. package/catalog/agent/rules/managed-tool-binaries.md +67 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
  6. package/catalog/agent/rules/pre-implementation.md +36 -20
  7. package/catalog/agent/rules/rtk-routing.md +22 -16
  8. package/catalog/agent/skills/autopilot/SKILL.md +4 -3
  9. package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
  10. package/catalog/agent/skills/verify/SKILL.md +12 -12
  11. package/catalog/compose/admin-blocks-registry.json +659 -0
  12. package/dist/esm/audit/hook-surface.js +5 -1
  13. package/dist/esm/audit/repo-guardrails.d.ts +8 -0
  14. package/dist/esm/audit/repo-guardrails.js +71 -1
  15. package/dist/esm/blueprint/core/parser.js +40 -16
  16. package/dist/esm/blueprint/core/validation/criteria.js +2 -1
  17. package/dist/esm/blueprint/execution/artifacts.js +8 -4
  18. package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
  19. package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
  20. package/dist/esm/blueprint/execution/metadata.js +8 -4
  21. package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
  22. package/dist/esm/blueprint/lifecycle/audit.js +89 -3
  23. package/dist/esm/blueprint/lifecycle/review-provenance.js +148 -40
  24. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
  25. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
  26. package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
  27. package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
  28. package/dist/esm/blueprint/markdown/helpers.js +10 -4
  29. package/dist/esm/blueprint/tracked-document/parser.js +3 -3
  30. package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
  31. package/dist/esm/blueprint/trust/gate-row.js +113 -0
  32. package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
  33. package/dist/esm/blueprint/trust/promotion.js +19 -19
  34. package/dist/esm/build/atomic-file-copy.d.ts +32 -0
  35. package/dist/esm/build/atomic-file-copy.js +44 -0
  36. package/dist/esm/build/cli-mcp-parity.js +3 -2
  37. package/dist/esm/build/package-manifest.js +7 -2
  38. package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
  39. package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
  40. package/dist/esm/cli/commands/audit-core.js +9 -1
  41. package/dist/esm/cli/commands/audit.js +16 -5
  42. package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
  43. package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
  44. package/dist/esm/cli/commands/compile.d.ts +13 -0
  45. package/dist/esm/cli/commands/compile.js +182 -62
  46. package/dist/esm/cli/commands/format.js +17 -1
  47. package/dist/esm/cli/commands/gain/index.js +10 -3
  48. package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
  49. package/dist/esm/cli/commands/init/index.js +29 -48
  50. package/dist/esm/cli/commands/init/mcp-spec.d.ts +54 -4
  51. package/dist/esm/cli/commands/init/mcp-spec.js +126 -22
  52. package/dist/esm/cli/commands/init/merge.js +30 -10
  53. package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
  54. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
  55. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
  56. package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
  57. package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
  58. package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
  59. package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
  60. package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
  61. package/dist/esm/cli/commands/package-manager.js +8 -2
  62. package/dist/esm/cli/commands/pr-help.js +1 -1
  63. package/dist/esm/cli/commands/pr.js +18 -4
  64. package/dist/esm/cli/commands/review.js +60 -6
  65. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
  66. package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
  67. package/dist/esm/cli/commands/worktree/router.js +5 -2
  68. package/dist/esm/cli/direct-provider-launch.js +12 -4
  69. package/dist/esm/compiler/flatten.d.ts +10 -1
  70. package/dist/esm/compiler/flatten.js +14 -4
  71. package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
  72. package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
  73. package/dist/esm/compose/compose.d.ts +46 -0
  74. package/dist/esm/compose/compose.js +250 -0
  75. package/dist/esm/compose/escape-html.d.ts +5 -0
  76. package/dist/esm/compose/escape-html.js +12 -0
  77. package/dist/esm/compose/ops-report-render.d.ts +5 -0
  78. package/dist/esm/compose/ops-report-render.js +71 -0
  79. package/dist/esm/compose/ops-report-schema.d.ts +42 -0
  80. package/dist/esm/compose/ops-report-schema.js +60 -0
  81. package/dist/esm/compose/preview.d.ts +14 -0
  82. package/dist/esm/compose/preview.js +32 -0
  83. package/dist/esm/compose/registry-resolve.d.ts +64 -0
  84. package/dist/esm/compose/registry-resolve.js +200 -0
  85. package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
  86. package/dist/esm/config/oxlint/import-hygiene.js +21 -8
  87. package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
  88. package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
  89. package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
  90. package/dist/esm/config/oxlint/oxlintrc.js +18 -21
  91. package/dist/esm/config/oxlint/path-roles.js +53 -0
  92. package/dist/esm/config/oxlint/query-patterns.js +2 -5
  93. package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
  94. package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
  95. package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
  96. package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
  97. package/dist/esm/daemon/domains/launch.d.ts +25 -0
  98. package/dist/esm/daemon/domains/launch.js +30 -1
  99. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
  100. package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
  101. package/dist/esm/daemon/handlers/product.d.ts +10 -0
  102. package/dist/esm/daemon/handlers/product.js +10 -1
  103. package/dist/esm/daemon/methods.d.ts +21 -1
  104. package/dist/esm/daemon/methods.js +20 -0
  105. package/dist/esm/daemon/protocol/contract.d.ts +58 -0
  106. package/dist/esm/daemon/protocol/contract.js +38 -0
  107. package/dist/esm/daemon/server.js +7 -0
  108. package/dist/esm/docs-linter/blueprint-plan.js +2 -2
  109. package/dist/esm/errors/wp-error.js +21 -2
  110. package/dist/esm/git/changed-files.d.ts +20 -0
  111. package/dist/esm/git/changed-files.js +63 -0
  112. package/dist/esm/hooks/doctor.d.ts +11 -0
  113. package/dist/esm/hooks/doctor.js +114 -45
  114. package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
  115. package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
  116. package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
  117. package/dist/esm/hooks/pretool-guard/runner.js +6 -0
  118. package/dist/esm/hooks/status/index.js +1 -1
  119. package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
  120. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
  121. package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
  122. package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
  123. package/dist/esm/mcp/blueprint/handlers/validate.js +6 -0
  124. package/dist/esm/mcp/blueprint/registration.js +1 -1
  125. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  126. package/dist/esm/mcp/tools/_names.js +4 -0
  127. package/dist/esm/mcp/tools/_registry.js +8 -0
  128. package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
  129. package/dist/esm/mcp/tools/_shared/redact.js +114 -8
  130. package/dist/esm/mcp/tools/gain.js +28 -2
  131. package/dist/esm/mcp/tools/release-progress.d.ts +9 -0
  132. package/dist/esm/mcp/tools/release-progress.js +110 -0
  133. package/dist/esm/mcp/tools/worktree.js +65 -3
  134. package/dist/esm/mcp/tools/wp-ui-blocks-list.d.ts +3 -0
  135. package/dist/esm/mcp/tools/wp-ui-blocks-list.js +43 -0
  136. package/dist/esm/mcp/tools/wp-ui-compose.d.ts +3 -0
  137. package/dist/esm/mcp/tools/wp-ui-compose.js +96 -0
  138. package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
  139. package/dist/esm/mcp/tools/wp-ui-preview.js +59 -0
  140. package/dist/esm/package.json +2 -0
  141. package/dist/esm/review/lifecycle-writers.d.ts +76 -0
  142. package/dist/esm/review/lifecycle-writers.js +330 -0
  143. package/dist/esm/review/subject.js +66 -32
  144. package/dist/esm/status/snapshot.d.ts +5 -0
  145. package/dist/esm/status/snapshot.js +27 -2
  146. package/dist/esm/test/shard-durations.json +0 -1
  147. package/dist/esm/test-helpers/global-setup.js +38 -3
  148. package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
  149. package/dist/esm/tool-runtime/managed-rtk.js +275 -0
  150. package/dist/esm/tool-runtime/resolve-runner.js +18 -194
  151. package/dist/esm/utils/package-root.d.ts +32 -0
  152. package/dist/esm/utils/package-root.js +82 -0
  153. package/dist/esm/worktrees/git-metadata.d.ts +41 -0
  154. package/dist/esm/worktrees/git-metadata.js +48 -0
  155. package/package.json +27 -21
  156. package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
  157. package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
  158. package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
  159. package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
  160. package/dist/esm/config/oxlint/index.d.ts +0 -22
  161. package/dist/esm/config/oxlint/index.js +0 -28
  162. package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
  163. package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
  164. package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
  165. package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
  166. package/dist/esm/output-transforms/rulesync.d.ts +0 -2
  167. package/dist/esm/output-transforms/rulesync.js +0 -79
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Single owner of the Trust Dossier "Promotion Gates" table row grammar.
3
+ *
4
+ * Two callers must agree on this grammar byte-for-byte:
5
+ *
6
+ * - the lifecycle WRITER `updateGateLastResult` (`./promotion.ts`), which stamps
7
+ * the `Last result` cell during `wp blueprint promote`; and
8
+ * - the plan-subject digest NORMALIZER `neutralizePromotionGateResults`
9
+ * (`#review/subject.js`), which replaces that same cell with a placeholder so
10
+ * the stamp is never hashed into a plan approval's subject.
11
+ *
12
+ * They each used to implement the grammar independently, and diverged on four
13
+ * axes at once — cell count (writer 4 or 5, normalizer 4 only), section scope
14
+ * (writer any table row in the document, normalizer inside `### Promotion Gates`
15
+ * only), trailing pipe (writer `trim().startsWith("|")` alone, normalizer both
16
+ * pipes), and trailing whitespace after the closing pipe (writer sliced the
17
+ * UNTRIMMED line, so it mis-counted a 4-cell row as 5 and appended a spurious
18
+ * empty cell). Every divergence made `digest(doc) !== digest(writer(doc))`,
19
+ * which destroys the plan approval the promotion is gating on. One owner,
20
+ * consumed from both sides, is the only structural fix; anything else lets them
21
+ * drift again.
22
+ *
23
+ * The grammar deliberately matches what `parsePromotionGatesTable` in
24
+ * `./dossier.ts` already enforces on committed documents: a row is the trimmed
25
+ * line starting AND ending with a pipe, cells are split on `|` and trimmed, and
26
+ * the accepted widths are 4 (legacy) and 5 (opt-in `Defer` column). A row this
27
+ * module rejects is a row the dossier parser reports as `malformed table row`,
28
+ * so it can never be a live gate — skipping it is the fail-safe direction.
29
+ */
30
+ /** Zero-based index of the lifecycle-owned `Last result` cell. */
31
+ export const LAST_RESULT_CELL_INDEX = 3;
32
+ /**
33
+ * Exact end-anchored heading match, mirroring `sectionBlock(markdown, 3, ...)`
34
+ * in `./dossier.ts` — `### Promotion Gates Notes` is NOT the owned section and
35
+ * its contents stay plan substance.
36
+ */
37
+ const PROMOTION_GATES_HEADING = /^###\s+Promotion Gates\s*$/iu;
38
+ /**
39
+ * The section ends at the next heading of ANY level. `sectionBlock` itself ends
40
+ * at the next heading of level <= 3, so a table placed after an intervening
41
+ * `####` heading would be a gate to the parser but not to this walk. The
42
+ * narrower scope is deliberate: it is the scope every committed plan digest was
43
+ * computed against (measured: zero tracked blueprint documents put gate rows in
44
+ * that gap), and the consequence of the mismatch is fail-closed — the writer
45
+ * leaves such a row unstamped, so `validateBlueprintTrust` rejects the promotion
46
+ * with a non-passing gate instead of silently leaking the stamp into the digest.
47
+ */
48
+ const ANY_HEADING = /^#{1,6}\s/u;
49
+ const ACCEPTED_ROW_WIDTHS = [4, 5];
50
+ /**
51
+ * Cells of one Promotion Gates table row, trimmed, or null when the line is not
52
+ * a row this grammar owns (no pipes, a single pipe, or an unsupported width).
53
+ */
54
+ export function parsePromotionGateCells(line) {
55
+ const body = line.trim();
56
+ if (body.length < 2 || !body.startsWith("|") || !body.endsWith("|"))
57
+ return null;
58
+ const cells = body
59
+ .slice(1, -1)
60
+ .split("|")
61
+ .map((cell) => cell.trim());
62
+ if (!ACCEPTED_ROW_WIDTHS.includes(cells.length))
63
+ return null;
64
+ return cells;
65
+ }
66
+ /**
67
+ * Canonical row bytes. Whitespace around a row (leading indent, padding inside
68
+ * cells, whitespace after the closing pipe) is not plan substance, so both
69
+ * callers re-emit this single canonical form. That is what keeps a stamped row
70
+ * and its normalized counterpart byte-identical, and it is exactly the form the
71
+ * normalizer has always emitted for a 4-cell row, so committed plan digests are
72
+ * unaffected.
73
+ */
74
+ export function formatPromotionGateRow(cells) {
75
+ return `| ${cells.join(" | ")} |`;
76
+ }
77
+ /** Copy `cells` with the lifecycle-owned `Last result` cell set to `value`. */
78
+ export function setLastResultCell(cells, value) {
79
+ const next = [...cells];
80
+ next[LAST_RESULT_CELL_INDEX] = value;
81
+ return next;
82
+ }
83
+ /**
84
+ * Rewrite every Promotion Gates table row (header, separator, and data rows
85
+ * alike) inside the `### Promotion Gates` subsection through `transform`.
86
+ * Returning the received `cells` reference unchanged leaves the line
87
+ * byte-identical; returning a new array re-emits the canonical row. Every line
88
+ * outside the section is always byte-identical.
89
+ */
90
+ export function mapPromotionGateRows(text, transform) {
91
+ const lines = text.split("\n");
92
+ let inGatesSection = false;
93
+ for (let index = 0; index < lines.length; index += 1) {
94
+ const line = lines[index] ?? "";
95
+ if (PROMOTION_GATES_HEADING.test(line)) {
96
+ inGatesSection = true;
97
+ continue;
98
+ }
99
+ if (inGatesSection && ANY_HEADING.test(line)) {
100
+ inGatesSection = false;
101
+ continue;
102
+ }
103
+ if (!inGatesSection)
104
+ continue;
105
+ const cells = parsePromotionGateCells(line);
106
+ if (!cells)
107
+ continue;
108
+ const next = transform(cells);
109
+ if (next !== cells)
110
+ lines[index] = formatPromotionGateRow(next);
111
+ }
112
+ return lines.join("\n");
113
+ }
@@ -19,3 +19,20 @@ export declare function assertPromotionTrustProofCurrent(input: Pick<PromotionTr
19
19
  export declare function runPromotionCommand(repoRoot: string, command: string, deps?: {
20
20
  spawn?: typeof spawnSync;
21
21
  }): void;
22
+ /**
23
+ * Exported for the plan-subject digest invariance suite
24
+ * (`src/review/lifecycle-writers.ts`, `src/review/subject-normalize.test.ts`).
25
+ * Those tests MUST call the production writer, never a hand-written mirror: a
26
+ * mirror can drift with the very bug under test and stay self-consistent, which
27
+ * is exactly how the 5-cell Promotion Gates leak survived the suite.
28
+ */
29
+ export declare function upsertReadinessValue(markdown: string, key: string, value: string): string;
30
+ /**
31
+ * Stamp the `Last result` cell of the gate named `gate`.
32
+ *
33
+ * Exported for the same reason as `upsertReadinessValue` above. The row grammar
34
+ * and the section scope both come from `./gate-row.js`, which the plan-subject
35
+ * digest normalizer consumes too — hand-rolling either here is what let the two
36
+ * sides diverge on four axes and destroy plan approvals on promotion.
37
+ */
38
+ export declare function updateGateLastResult(markdown: string, gate: string, result: string): string;
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { validateBlueprintTrust } from "./validator.js";
5
5
  import { parseTrustDossier } from "./dossier.js";
6
+ import { mapPromotionGateRows, setLastResultCell } from "./gate-row.js";
6
7
  import { parseAllowedWpCommand, wpTestFileTargets } from "./gates.js";
7
8
  import { ALLOWED_WP_COMMAND_TIMEOUT_MS, executeAllowedWpCommand, tailBounded, } from "./command-runner.js";
8
9
  export { parseAllowedWpCommand };
@@ -138,7 +139,14 @@ function formatPromotionGateFailure(result) {
138
139
  details.push(`log=${result.logPath}`);
139
140
  return `Promotion gate failed (${result.command}): ${details.join("; ")}`;
140
141
  }
141
- function upsertReadinessValue(markdown, key, value) {
142
+ /**
143
+ * Exported for the plan-subject digest invariance suite
144
+ * (`src/review/lifecycle-writers.ts`, `src/review/subject-normalize.test.ts`).
145
+ * Those tests MUST call the production writer, never a hand-written mirror: a
146
+ * mirror can drift with the very bug under test and stay self-consistent, which
147
+ * is exactly how the 5-cell Promotion Gates leak survived the suite.
148
+ */
149
+ export function upsertReadinessValue(markdown, key, value) {
142
150
  const re = new RegExp(`^- ${key}: .*$`, "mu");
143
151
  if (re.test(markdown))
144
152
  return markdown.replace(re, `- ${key}: ${value}`);
@@ -152,22 +160,14 @@ function upsertReadinessValue(markdown, key, value) {
152
160
  lines.splice(insertIndex, 0, `- ${key}: ${value}`);
153
161
  return lines.join("\n");
154
162
  }
155
- function updateGateLastResult(markdown, gate, result) {
156
- const lines = markdown.split("\n");
157
- for (let i = 0; i < lines.length; i++) {
158
- const line = lines[i] ?? "";
159
- const cells = line.trim().startsWith("|")
160
- ? line
161
- .slice(1, -1)
162
- .split("|")
163
- .map((c) => c.trim())
164
- : [];
165
- if (cells[0] === gate && cells.length === 4) {
166
- lines[i] = `| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${result} |`;
167
- }
168
- else if (cells[0] === gate && cells.length === 5) {
169
- lines[i] = `| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${result} | ${cells[4]} |`;
170
- }
171
- }
172
- return lines.join("\n");
163
+ /**
164
+ * Stamp the `Last result` cell of the gate named `gate`.
165
+ *
166
+ * Exported for the same reason as `upsertReadinessValue` above. The row grammar
167
+ * and the section scope both come from `./gate-row.js`, which the plan-subject
168
+ * digest normalizer consumes too — hand-rolling either here is what let the two
169
+ * sides diverge on four axes and destroy plan approvals on promotion.
170
+ */
171
+ export function updateGateLastResult(markdown, gate, result) {
172
+ return mapPromotionGateRows(markdown, (cells) => cells[0] === gate ? setLastResultCell(cells, result) : cells);
173
173
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copy `source` to `destination` via copy-to-temp + atomic rename.
3
+ *
4
+ * A plain `copyFileSync(source, destination)` opens `destination` directly
5
+ * for writing. If another process currently has `destination` open for
6
+ * execution (a running compiled binary, a loaded native addon), POSIX
7
+ * returns `ETXTBSY` ("text file is busy"). This is not a flake: it is a
8
+ * deterministic race whenever a staging step runs concurrently with a
9
+ * process that execs the file being replaced (for example a vitest shard
10
+ * running the freshly staged `wp` runtime binary while another shard
11
+ * restages it).
12
+ *
13
+ * `rename(2)` is atomic within a filesystem and is legal even onto a file
14
+ * another process currently has open for execution: the running process
15
+ * keeps its already-open inode (and keeps running unaffected), while any
16
+ * new open/exec of `destination` resolves to the new file. Copying to a
17
+ * unique temp file beside `destination` (same directory) keeps the rename
18
+ * on the same filesystem so it stays atomic, then renaming over the
19
+ * destination replaces it without ever truncating a file that may be
20
+ * actively executing. `copyFileSync` preserves the source file's mode on
21
+ * the temp file, so there is no non-executable window between the rename
22
+ * and any later `chmodSync` a caller performs.
23
+ *
24
+ * On any failure (source read, temp write, or rename) the temp file is
25
+ * removed via `rmSync(..., { force: true })` (which tolerates the file not
26
+ * existing) before the error is rethrown.
27
+ *
28
+ * This guarantee is POSIX-only: on Windows, `renameSync` onto a file that
29
+ * is currently open for execution fails with `EPERM` for the same reason
30
+ * `copyFileSync` would, so this helper does not remove the race there.
31
+ */
32
+ export declare function copyFileAtomic(source: string, destination: string): void;
@@ -0,0 +1,44 @@
1
+ import { copyFileSync, renameSync, rmSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ /**
4
+ * Copy `source` to `destination` via copy-to-temp + atomic rename.
5
+ *
6
+ * A plain `copyFileSync(source, destination)` opens `destination` directly
7
+ * for writing. If another process currently has `destination` open for
8
+ * execution (a running compiled binary, a loaded native addon), POSIX
9
+ * returns `ETXTBSY` ("text file is busy"). This is not a flake: it is a
10
+ * deterministic race whenever a staging step runs concurrently with a
11
+ * process that execs the file being replaced (for example a vitest shard
12
+ * running the freshly staged `wp` runtime binary while another shard
13
+ * restages it).
14
+ *
15
+ * `rename(2)` is atomic within a filesystem and is legal even onto a file
16
+ * another process currently has open for execution: the running process
17
+ * keeps its already-open inode (and keeps running unaffected), while any
18
+ * new open/exec of `destination` resolves to the new file. Copying to a
19
+ * unique temp file beside `destination` (same directory) keeps the rename
20
+ * on the same filesystem so it stays atomic, then renaming over the
21
+ * destination replaces it without ever truncating a file that may be
22
+ * actively executing. `copyFileSync` preserves the source file's mode on
23
+ * the temp file, so there is no non-executable window between the rename
24
+ * and any later `chmodSync` a caller performs.
25
+ *
26
+ * On any failure (source read, temp write, or rename) the temp file is
27
+ * removed via `rmSync(..., { force: true })` (which tolerates the file not
28
+ * existing) before the error is rethrown.
29
+ *
30
+ * This guarantee is POSIX-only: on Windows, `renameSync` onto a file that
31
+ * is currently open for execution fails with `EPERM` for the same reason
32
+ * `copyFileSync` would, so this helper does not remove the race there.
33
+ */
34
+ export function copyFileAtomic(source, destination) {
35
+ const tmpDestination = `${destination}.tmp-${process.pid}-${randomUUID()}`;
36
+ try {
37
+ copyFileSync(source, tmpDestination);
38
+ renameSync(tmpDestination, destination);
39
+ }
40
+ catch (error) {
41
+ rmSync(tmpDestination, { force: true });
42
+ throw error;
43
+ }
44
+ }
@@ -52,8 +52,9 @@ export const CLI_MCP_PARITY_CLASSIFICATION = {
52
52
  },
53
53
  "release-progress": {
54
54
  command: "release-progress",
55
- classification: "follow-up-candidate",
56
- reason: "operator/agent Release workflow phase reporter (detect/native-matrix/publish); CLI-only, uses gh API",
55
+ classification: "existing-mcp",
56
+ tools: ["wp_release_progress"],
57
+ reason: "Release workflow phase reporter (detect/native-matrix/publish) is available as wp_release_progress MCP and CLI",
57
58
  },
58
59
  cleanup: {
59
60
  command: "cleanup",
@@ -1,8 +1,8 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { parse as parseYaml } from "yaml";
4
- import { assertBuiltBlueprintMigrationSqlAssets } from "./blueprint-migration-assets.js";
5
- import { assertBuiltSastRuleAssets } from "./sast-rule-assets.js";
4
+ import { assertBuiltBlueprintMigrationSqlAssets, syncBlueprintMigrationSqlAssets, } from "./blueprint-migration-assets.js";
5
+ import { assertBuiltSastRuleAssets, syncSastRuleAssets } from "./sast-rule-assets.js";
6
6
  import { SESSION_MEMORY_NATIVE_TARGETS } from "#session-memory/native-targets.js";
7
7
  import { RUNTIME_TARGETS } from "./runtime-targets.js";
8
8
  const DEPENDENCY_SECTIONS = [
@@ -304,6 +304,11 @@ export function preparePackedManifest(rootDir, options = {}) {
304
304
  // out-of-org consumers cannot resolve.
305
305
  const packedManifest = createPackedManifest(manifest, readWorkspaceCatalogs(workspacePath), options);
306
306
  if (options.assertBlueprintMigrationAssets ?? true) {
307
+ // Sync authoring sources into dist before assert so pack/prepack paths
308
+ // (version dry-run, npm pack) do not require a full `pnpm run build` first.
309
+ // Assert still fails closed on missing source or content drift.
310
+ syncBlueprintMigrationSqlAssets(rootDir);
311
+ syncSastRuleAssets(rootDir);
307
312
  assertBuiltBlueprintMigrationSqlAssets(rootDir);
308
313
  assertBuiltSastRuleAssets(rootDir);
309
314
  }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bun
2
+ export declare const AGENT_CONFIG_OXLINT_DIST: string;
3
+ export declare const AGENT_KIT_OXLINT_DIST: string;
4
+ /** Plugin basenames that ship as sibling .js next to oxlintrc.json (not path-roles). */
5
+ export declare const VENDOR_PLUGIN_BASENAMES: readonly ["code-safety", "foundation-purity", "graphql-conventions", "import-hygiene", "monorepo-paths", "query-patterns", "testing-quality", "tier-boundaries"];
6
+ export declare function vendorOxlintPlugins(sourceDir?: string, destDir?: string): {
7
+ copied: string[];
8
+ };
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Vendor compiled oxlint policy plugins from agent-config into agent-kit dist.
4
+ *
5
+ * Consumer SSOT remains @webpresso/agent-config/oxlint/*. agent-kit's inject
6
+ * oxlintrc uses sibling-relative jsPlugins; we copy the built plugin files so
7
+ * the published kit has zero runtime dependency on agent-config (no re-export
8
+ * stubs, no pack chicken-egg).
9
+ */
10
+ import { cpSync, existsSync, mkdirSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ export const AGENT_CONFIG_OXLINT_DIST = join(process.cwd(), "packages/agent-config/dist/esm/oxlint");
13
+ export const AGENT_KIT_OXLINT_DIST = join(process.cwd(), "dist/esm/config/oxlint");
14
+ /** Plugin basenames that ship as sibling .js next to oxlintrc.json (not path-roles). */
15
+ export const VENDOR_PLUGIN_BASENAMES = [
16
+ "code-safety",
17
+ "foundation-purity",
18
+ "graphql-conventions",
19
+ "import-hygiene",
20
+ "monorepo-paths",
21
+ "query-patterns",
22
+ "testing-quality",
23
+ "tier-boundaries",
24
+ ];
25
+ export function vendorOxlintPlugins(sourceDir = AGENT_CONFIG_OXLINT_DIST, destDir = AGENT_KIT_OXLINT_DIST) {
26
+ if (!existsSync(sourceDir)) {
27
+ throw new Error(`vendor-oxlint-plugins: missing agent-config oxlint dist at ${sourceDir}. Build @webpresso/agent-config first.`);
28
+ }
29
+ mkdirSync(destDir, { recursive: true });
30
+ const copied = [];
31
+ for (const name of VENDOR_PLUGIN_BASENAMES) {
32
+ const src = join(sourceDir, `${name}.js`);
33
+ if (!existsSync(src)) {
34
+ throw new Error(`vendor-oxlint-plugins: missing compiled plugin ${src}`);
35
+ }
36
+ const dest = join(destDir, `${name}.js`);
37
+ cpSync(src, dest);
38
+ copied.push(name);
39
+ // path-roles is imported by several plugins as a sibling — vendor it too.
40
+ }
41
+ // Always vendor path-roles (shared classifier used via relative imports inside plugins).
42
+ const pathRoles = join(sourceDir, "path-roles.js");
43
+ if (!existsSync(pathRoles)) {
44
+ throw new Error(`vendor-oxlint-plugins: missing ${pathRoles}`);
45
+ }
46
+ cpSync(pathRoles, join(destDir, "path-roles.js"));
47
+ copied.push("path-roles");
48
+ return { copied };
49
+ }
50
+ if (import.meta.main) {
51
+ const { copied } = vendorOxlintPlugins();
52
+ process.stdout.write(`vendored oxlint plugins: ${copied.join(", ")}\n`);
53
+ }
@@ -79,7 +79,15 @@ export async function runAuditDispatch(auditKind, targets, options, deps) {
79
79
  case "blueprint-pr-coverage": {
80
80
  const { auditBlueprintPrCoverage } = await import("#audit/blueprint-pr-coverage");
81
81
  const root = options.root ?? target ?? deps.root;
82
- const baseRef = options.baseRef ?? options.base;
82
+ const explicitBaseRef = options.baseRef ?? options.base;
83
+ // Same pinned-base hazard as the branch-scoped audits: CI's
84
+ // `github.event.pull_request.base.sha` stops being the fork point once the
85
+ // branch is rebased, and this audit diffs `<baseRef>...HEAD`. Narrow it to
86
+ // the real fork point so coverage is judged on this PR's own files.
87
+ const { resolveBranchDiffBase } = await import("#git/changed-files");
88
+ const baseRef = explicitBaseRef
89
+ ? resolveBranchDiffBase(root, explicitBaseRef)
90
+ : explicitBaseRef;
83
91
  const result = auditBlueprintPrCoverage(root, { baseRef, headRef: options.headRef });
84
92
  return { kind: "repo-result", name: "blueprint-pr-coverage", result };
85
93
  }
@@ -9,7 +9,7 @@ import { STRYKER_COMMAND } from "#audit/run-stryker.js";
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
10
  import path from "node:path";
11
11
  import { addAffectedOptions, resolveAffectedTargets } from "#git/affected";
12
- import { defaultBranchBaseRef, getBranchRemovedFiles, getStagedRemovedFiles, } from "#git/changed-files";
12
+ import { defaultBranchBaseRef, getBranchRemovedFiles, getStagedRemovedFiles, resolveBranchDiffBase, } from "#git/changed-files";
13
13
  import { runAuditDispatch } from "#cli/commands/audit-core.js";
14
14
  import { createCliLogSink } from "#cli/commands/quality-log-store.js";
15
15
  import { emitCliCommandOutput, runLoggedChildCommand } from "#cli/commands/quality-runner.js";
@@ -284,6 +284,17 @@ export function registerAuditCommand(cli) {
284
284
  let dispatchOptions = options;
285
285
  let auditScope = "full";
286
286
  let changeRemovesFile = false;
287
+ // A caller-supplied `--base` is a PINNED sha (CI passes
288
+ // `github.event.pull_request.base.sha`, which does not advance with the
289
+ // base branch). After a rebase it stops being the fork point, so scoping
290
+ // off it silently absorbs everything merged into the base branch since
291
+ // the PR opened. Narrow it to the real fork point once, and use that one
292
+ // value everywhere the branch scope is derived so the affected file set,
293
+ // the removal probe, and the audit's own base-vs-head range agree.
294
+ const explicitBaseRef = options.baseRef ?? options.base;
295
+ const branchBaseRef = branch && explicitBaseRef
296
+ ? resolveBranchDiffBase(auditRoot, explicitBaseRef)
297
+ : explicitBaseRef;
287
298
  if (affected || branch) {
288
299
  if (kind &&
289
300
  AUDIT_KINDS.includes(kind) &&
@@ -309,7 +320,7 @@ export function registerAuditCommand(cli) {
309
320
  cwd: auditRoot,
310
321
  affected,
311
322
  branch,
312
- baseRef: options.baseRef ?? options.base,
323
+ baseRef: branchBaseRef,
313
324
  explicitTargets: [],
314
325
  policy: "fail-closed",
315
326
  mapChangedFiles: (files) => files,
@@ -360,7 +371,7 @@ export function registerAuditCommand(cli) {
360
371
  changeRemovesFile = affected
361
372
  ? affectedChangeRemovesFile(auditRoot, {
362
373
  branch,
363
- baseRef: options.baseRef ?? options.base,
374
+ baseRef: branchBaseRef,
364
375
  })
365
376
  : false;
366
377
  if (resolution.kind === "empty" && !changeRemovesFile) {
@@ -392,7 +403,7 @@ export function registerAuditCommand(cli) {
392
403
  changedOnly: true,
393
404
  affectedFiles: [],
394
405
  root: options.root ?? resolution.cwd,
395
- baseRef: options.baseRef ?? options.base,
406
+ baseRef: branchBaseRef,
396
407
  };
397
408
  }
398
409
  if (resolution.kind === "scoped") {
@@ -404,7 +415,7 @@ export function registerAuditCommand(cli) {
404
415
  changedOnly: true,
405
416
  affectedFiles: [...resolution.targets],
406
417
  root: options.root ?? resolution.cwd,
407
- baseRef: options.baseRef ?? options.base,
418
+ baseRef: branchBaseRef,
408
419
  };
409
420
  }
410
421
  }
@@ -85,6 +85,21 @@ export interface PromoteBlueprintResult {
85
85
  readonly newPath: string;
86
86
  readonly message: string;
87
87
  }
88
+ /**
89
+ * Update `status:` in YAML frontmatter. Preserves everything else verbatim.
90
+ *
91
+ * Exported for the plan-subject digest invariance suite
92
+ * (`src/review/lifecycle-writers.ts`, `src/review/subject-normalize.test.ts`),
93
+ * which must exercise the production writer rather than a hand-written mirror.
94
+ */
95
+ export declare function updateFrontmatterStatus(content: string, newStatus: string): string;
96
+ /**
97
+ * Add or update `completed_at:` in YAML frontmatter.
98
+ * Inserts after the `status:` line if not already present.
99
+ *
100
+ * Exported for the same reason as `updateFrontmatterStatus` above.
101
+ */
102
+ export declare function upsertCompletedAt(content: string, isoDate: string): string;
88
103
  /**
89
104
  * Advance a task's status in its blueprint markdown document, then re-ingest.
90
105
  *
@@ -188,15 +188,21 @@ async function forceReIngestBlueprint(cwd, slug, documentPath) {
188
188
  }
189
189
  /**
190
190
  * Update `status:` in YAML frontmatter. Preserves everything else verbatim.
191
+ *
192
+ * Exported for the plan-subject digest invariance suite
193
+ * (`src/review/lifecycle-writers.ts`, `src/review/subject-normalize.test.ts`),
194
+ * which must exercise the production writer rather than a hand-written mirror.
191
195
  */
192
- function updateFrontmatterStatus(content, newStatus) {
196
+ export function updateFrontmatterStatus(content, newStatus) {
193
197
  return content.replace(/^(status:\s*)(['"]?)[^'"\r\n]+?(['"]?)(\s*)$/m, `$1${newStatus}$4`);
194
198
  }
195
199
  /**
196
200
  * Add or update `completed_at:` in YAML frontmatter.
197
201
  * Inserts after the `status:` line if not already present.
202
+ *
203
+ * Exported for the same reason as `updateFrontmatterStatus` above.
198
204
  */
199
- function upsertCompletedAt(content, isoDate) {
205
+ export function upsertCompletedAt(content, isoDate) {
200
206
  // If already present, update it
201
207
  if (/^completed_at:/m.test(content)) {
202
208
  return content.replace(/^(completed_at:\s*).*$/m, `$1'${isoDate}'`);
@@ -1,4 +1,17 @@
1
1
  import type { CAC } from "cac";
2
+ import { type ModuleResolver } from "#shared-utils/package-root.js";
3
+ export declare function resolveRulesyncBinFromAgentKit(resolver?: ModuleResolver): string | null;
4
+ /**
5
+ * The rulesync version agent-kit itself resolves — the only honest baseline to
6
+ * compare a consumer's install against.
7
+ *
8
+ * This used to be a hand-maintained `PINNED_RULESYNC_VERSION` literal, which
9
+ * drifted five majors behind the declared dependency (9.4.0 vs 14.0.1). The
10
+ * mismatch warning then fired on every single run, including inside agent-kit
11
+ * itself, which trains readers to ignore the one warning meant to catch a real
12
+ * mismatch. Deriving it from the resolved package makes that drift impossible.
13
+ */
14
+ export declare function resolveBundledRulesyncVersion(resolver?: ModuleResolver): string | null;
2
15
  export interface CompileResult {
3
16
  readonly ok: boolean;
4
17
  readonly targets: readonly string[];