@webpresso/agent-kit 3.3.3 → 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.
- package/THIRD-PARTY-NOTICES.md +12 -10
- package/catalog/agent/rules/changeset-release.md +15 -4
- package/catalog/agent/rules/managed-tool-binaries.md +67 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
- package/catalog/agent/rules/pre-implementation.md +36 -20
- package/catalog/agent/rules/rtk-routing.md +22 -16
- package/catalog/agent/skills/autopilot/SKILL.md +4 -3
- package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
- package/dist/esm/audit/hook-surface.js +5 -1
- package/dist/esm/audit/repo-guardrails.d.ts +8 -0
- package/dist/esm/audit/repo-guardrails.js +71 -1
- package/dist/esm/blueprint/core/parser.js +38 -12
- package/dist/esm/blueprint/execution/artifacts.js +8 -4
- package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
- package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
- package/dist/esm/blueprint/execution/metadata.js +8 -4
- package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
- package/dist/esm/blueprint/lifecycle/audit.js +89 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.js +63 -10
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
- package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
- package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
- package/dist/esm/blueprint/markdown/helpers.js +10 -4
- package/dist/esm/blueprint/tracked-document/parser.js +3 -3
- package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
- package/dist/esm/blueprint/trust/gate-row.js +113 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
- package/dist/esm/blueprint/trust/promotion.js +19 -19
- package/dist/esm/build/atomic-file-copy.d.ts +32 -0
- package/dist/esm/build/atomic-file-copy.js +44 -0
- package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
- package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
- package/dist/esm/cli/commands/compile.d.ts +13 -0
- package/dist/esm/cli/commands/compile.js +182 -62
- package/dist/esm/cli/commands/format.js +17 -1
- package/dist/esm/cli/commands/gain/index.js +10 -3
- package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
- package/dist/esm/cli/commands/init/index.js +29 -48
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
- package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
- package/dist/esm/cli/commands/init/merge.js +30 -10
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
- package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
- package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
- package/dist/esm/cli/commands/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.js +60 -6
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
- package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
- package/dist/esm/cli/commands/worktree/router.js +5 -2
- package/dist/esm/cli/direct-provider-launch.js +12 -4
- package/dist/esm/compiler/flatten.d.ts +10 -1
- package/dist/esm/compiler/flatten.js +14 -4
- package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
- package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
- package/dist/esm/compose/compose.d.ts +8 -0
- package/dist/esm/compose/compose.js +115 -8
- package/dist/esm/compose/ops-report-schema.js +17 -3
- package/dist/esm/compose/registry-resolve.d.ts +17 -0
- package/dist/esm/compose/registry-resolve.js +78 -11
- package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
- package/dist/esm/config/oxlint/import-hygiene.js +21 -8
- package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
- package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
- package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
- package/dist/esm/config/oxlint/oxlintrc.js +18 -21
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
- package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
- package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
- package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
- package/dist/esm/daemon/domains/launch.d.ts +25 -0
- package/dist/esm/daemon/domains/launch.js +30 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
- package/dist/esm/daemon/handlers/product.d.ts +10 -0
- package/dist/esm/daemon/handlers/product.js +10 -1
- package/dist/esm/daemon/methods.d.ts +21 -1
- package/dist/esm/daemon/methods.js +20 -0
- package/dist/esm/daemon/protocol/contract.d.ts +58 -0
- package/dist/esm/daemon/protocol/contract.js +38 -0
- package/dist/esm/daemon/server.js +7 -0
- package/dist/esm/docs-linter/blueprint-plan.js +2 -2
- package/dist/esm/errors/wp-error.js +21 -2
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
- package/dist/esm/hooks/pretool-guard/runner.js +6 -0
- package/dist/esm/hooks/status/index.js +1 -1
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
- package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
- package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
- package/dist/esm/mcp/blueprint/registration.js +1 -1
- package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
- package/dist/esm/mcp/tools/_shared/redact.js +114 -8
- package/dist/esm/mcp/tools/gain.js +28 -2
- package/dist/esm/mcp/tools/worktree.js +65 -3
- package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
- package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
- package/dist/esm/review/lifecycle-writers.d.ts +76 -0
- package/dist/esm/review/lifecycle-writers.js +330 -0
- package/dist/esm/review/subject.js +66 -32
- package/dist/esm/status/snapshot.d.ts +5 -0
- package/dist/esm/status/snapshot.js +27 -2
- package/dist/esm/test/shard-durations.json +0 -1
- package/dist/esm/test-helpers/global-setup.js +38 -3
- package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
- package/dist/esm/tool-runtime/managed-rtk.js +275 -0
- package/dist/esm/tool-runtime/resolve-runner.js +18 -194
- package/dist/esm/utils/package-root.d.ts +32 -0
- package/dist/esm/utils/package-root.js +82 -0
- package/dist/esm/worktrees/git-metadata.d.ts +41 -0
- package/dist/esm/worktrees/git-metadata.js +48 -0
- package/package.json +16 -21
- package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
- package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
- package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
- package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
- package/dist/esm/config/oxlint/index.d.ts +0 -22
- package/dist/esm/config/oxlint/index.js +0 -28
- package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
- package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
- package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
- package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
- package/dist/esm/output-transforms/rulesync.d.ts +0 -2
- package/dist/esm/output-transforms/rulesync.js +0 -79
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single owner of the task-body `**Blocked:** <reason>` line grammar.
|
|
3
|
+
*
|
|
4
|
+
* Both sides of the plan-approval contract read this file: `updateBlockedReason`
|
|
5
|
+
* (`#markdown/helpers.js`) writes the line, and `neutralizeTaskBlockedReason`
|
|
6
|
+
* (`#review/subject.js`) removes it before the plan-subject digest is taken. That
|
|
7
|
+
* pairing is exactly the arrangement the Promotion Gates row grammar already uses
|
|
8
|
+
* (`#trust/gate-row.js`), and for the same reason: when a writer and the
|
|
9
|
+
* normalizer spell the same grammar twice, they drift, the lifecycle stamp
|
|
10
|
+
* survives into the digest, and the plan approval the promotion gates on is
|
|
11
|
+
* silently dismissed.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately dependency-free so any module on either side can import it
|
|
14
|
+
* without risking a cycle.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Regex source for a whole `**Blocked:** …` line, anchored to line bounds.
|
|
18
|
+
*
|
|
19
|
+
* Anchoring is load-bearing in both directions. Blueprint prose legitimately
|
|
20
|
+
* mentions the marker mid-sentence ("add a non-empty **Blocked:** line with the
|
|
21
|
+
* reason") — those mentions are plan substance, and an unanchored pattern would
|
|
22
|
+
* have the writer overwrite them and the normalizer strip them.
|
|
23
|
+
*/
|
|
24
|
+
export const BLOCKED_LINE_SOURCE = String.raw `^\*\*Blocked:\*\*.*$`;
|
|
25
|
+
/**
|
|
26
|
+
* Build a matcher for the blocked line. Mirrors the `buildTaskHeadingRegex`
|
|
27
|
+
* convention in `./task-heading.js`: callers pick the flags they need, but the
|
|
28
|
+
* pattern itself has exactly one definition.
|
|
29
|
+
*/
|
|
30
|
+
export function buildBlockedLineRegex(flags = "imu") {
|
|
31
|
+
return new RegExp(BLOCKED_LINE_SOURCE, flags);
|
|
32
|
+
}
|
|
33
|
+
const SINGLE_LINE_MATCHER = buildBlockedLineRegex("iu");
|
|
34
|
+
/** True when `line` IS a blocked-reason line, not merely a line that mentions one. */
|
|
35
|
+
export function isBlockedLine(line) {
|
|
36
|
+
return SINGLE_LINE_MATCHER.test(line);
|
|
37
|
+
}
|
|
38
|
+
const MARKER_LENGTH = "**Blocked:**".length;
|
|
39
|
+
/**
|
|
40
|
+
* Serialize the blocked line. The writer-side counterpart to
|
|
41
|
+
* {@link isBlockedLine}: every emitter goes through here so the written form and
|
|
42
|
+
* the parsed form cannot diverge.
|
|
43
|
+
*/
|
|
44
|
+
export function formatBlockedLine(reason) {
|
|
45
|
+
return `**Blocked:** ${reason}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read the reason off the first blocked line in a task section, or `undefined`
|
|
49
|
+
* when the section has none.
|
|
50
|
+
*
|
|
51
|
+
* The reader counterpart to {@link formatBlockedLine}. Three parsers used to
|
|
52
|
+
* carry their own unanchored copy of this pattern (`src/blueprint/core/parser.ts`,
|
|
53
|
+
* `src/blueprint/tracked-document/parser.ts`, `src/docs-linter/blueprint-plan.ts`),
|
|
54
|
+
* so a task whose prose merely mentioned the marker was read as genuinely
|
|
55
|
+
* blocked and the lifecycle audit failed the document.
|
|
56
|
+
*/
|
|
57
|
+
export function extractBlockedReason(section) {
|
|
58
|
+
const match = buildBlockedLineRegex().exec(section);
|
|
59
|
+
if (!match)
|
|
60
|
+
return undefined;
|
|
61
|
+
const reason = match[0].slice(MARKER_LENGTH).trim();
|
|
62
|
+
return reason === "" ? undefined : reason;
|
|
63
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* produces identical output (for the same inputs).
|
|
9
9
|
*/
|
|
10
10
|
import { escapeRegex } from "#utils/string";
|
|
11
|
+
import { BLOCKED_LINE_SOURCE, buildBlockedLineRegex, formatBlockedLine } from "./blocked-line.js";
|
|
11
12
|
import { buildTaskHeaderRegexForId, buildTaskSectionBoundaryRegex, TASK_HEADING_PREFIX_PATTERN, } from "./task-heading.js";
|
|
12
13
|
function buildTaskPattern(taskId) {
|
|
13
14
|
return new RegExp(`(####\\s+${TASK_HEADING_PREFIX_PATTERN}Task\\s+${escapeRegex(taskId)}[:\\s].*?)(?=\\n####|\\n###|$)`, "s");
|
|
@@ -65,7 +66,11 @@ export function updateBlockedReason(content, taskId, reason) {
|
|
|
65
66
|
const taskPattern = buildTaskPattern(taskId);
|
|
66
67
|
return content.replace(taskPattern, (section) => {
|
|
67
68
|
const trimmedReason = reason.trim();
|
|
68
|
-
|
|
69
|
+
// Grammar comes from ./blocked-line.js, which the plan-subject normalizer
|
|
70
|
+
// consumes too. Line-anchored on purpose: a task whose prose mentions
|
|
71
|
+
// `**Blocked:**` mid-sentence is plan substance, and the previous
|
|
72
|
+
// unanchored pattern would have overwritten it.
|
|
73
|
+
const blockedPattern = new RegExp(String.raw `\n?${BLOCKED_LINE_SOURCE}\n*`, "imu");
|
|
69
74
|
if (!trimmedReason) {
|
|
70
75
|
// Preserve the section's trailing whitespace: trimEnd on the whole
|
|
71
76
|
// section would also delete the blank line separating this task from the
|
|
@@ -76,9 +81,10 @@ export function updateBlockedReason(content, taskId, reason) {
|
|
|
76
81
|
.replace(/\n{3,}/g, "\n\n")
|
|
77
82
|
.trimEnd() + trailingWhitespace);
|
|
78
83
|
}
|
|
79
|
-
const blockedLine =
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
const blockedLine = formatBlockedLine(trimmedReason);
|
|
85
|
+
const existingLine = buildBlockedLineRegex();
|
|
86
|
+
if (existingLine.test(section)) {
|
|
87
|
+
return section.replace(existingLine, blockedLine);
|
|
82
88
|
}
|
|
83
89
|
return insertAfterStatusLine(section, blockedLine);
|
|
84
90
|
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Git-Native SSoT: Checkbox state derives status, no external state.
|
|
8
8
|
*/
|
|
9
|
+
import { extractBlockedReason } from "#markdown/blocked-line";
|
|
9
10
|
import { isTaskHeaderLine } from "#markdown/task-heading";
|
|
10
11
|
/**
|
|
11
12
|
* Extract checkbox status from a markdown section and derive task state.
|
|
@@ -100,10 +101,9 @@ export function extractDepends(section) {
|
|
|
100
101
|
* ```
|
|
101
102
|
*/
|
|
102
103
|
export function extractBlocked(section) {
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
104
|
+
const blockedText = extractBlockedReason(section);
|
|
105
|
+
if (blockedText === undefined)
|
|
105
106
|
return undefined;
|
|
106
|
-
const blockedText = blockedMatch[1].trim();
|
|
107
107
|
// Handle missing/empty reason gracefully
|
|
108
108
|
if (blockedText === "" || blockedText.toLowerCase() === "none") {
|
|
109
109
|
return undefined;
|
|
@@ -0,0 +1,55 @@
|
|
|
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 declare const LAST_RESULT_CELL_INDEX = 3;
|
|
32
|
+
/**
|
|
33
|
+
* Cells of one Promotion Gates table row, trimmed, or null when the line is not
|
|
34
|
+
* a row this grammar owns (no pipes, a single pipe, or an unsupported width).
|
|
35
|
+
*/
|
|
36
|
+
export declare function parsePromotionGateCells(line: string): readonly string[] | null;
|
|
37
|
+
/**
|
|
38
|
+
* Canonical row bytes. Whitespace around a row (leading indent, padding inside
|
|
39
|
+
* cells, whitespace after the closing pipe) is not plan substance, so both
|
|
40
|
+
* callers re-emit this single canonical form. That is what keeps a stamped row
|
|
41
|
+
* and its normalized counterpart byte-identical, and it is exactly the form the
|
|
42
|
+
* normalizer has always emitted for a 4-cell row, so committed plan digests are
|
|
43
|
+
* unaffected.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatPromotionGateRow(cells: readonly string[]): string;
|
|
46
|
+
/** Copy `cells` with the lifecycle-owned `Last result` cell set to `value`. */
|
|
47
|
+
export declare function setLastResultCell(cells: readonly string[], value: string): readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* Rewrite every Promotion Gates table row (header, separator, and data rows
|
|
50
|
+
* alike) inside the `### Promotion Gates` subsection through `transform`.
|
|
51
|
+
* Returning the received `cells` reference unchanged leaves the line
|
|
52
|
+
* byte-identical; returning a new array re-emits the canonical row. Every line
|
|
53
|
+
* outside the section is always byte-identical.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mapPromotionGateRows(text: string, transform: (cells: readonly string[]) => readonly string[]): string;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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[];
|