@webpresso/agent-kit 3.3.3 → 3.3.5
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/agents.js +126 -26
- 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/dossier.js +41 -1
- 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 +11 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +48 -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 +32 -52
- 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.d.ts +1 -0
- package/dist/esm/cli/commands/review.js +137 -7
- package/dist/esm/cli/commands/sync.d.ts +8 -6
- package/dist/esm/cli/commands/sync.js +11 -9
- 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 +19 -31
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/content/dispatch.js +4 -0
- package/dist/esm/content/loader.d.ts +1 -1
- package/dist/esm/content/loader.js +10 -6
- package/dist/esm/content/skill-policy.d.ts +11 -0
- package/dist/esm/content/skill-policy.js +33 -0
- 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 +68 -5
- 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/pretool-guard/validators/worktree-discipline.js +62 -3
- 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/authority.js +56 -1
- package/dist/esm/review/delivery-verifier.d.ts +4 -0
- package/dist/esm/review/delivery-verifier.js +9 -1
- 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/symlinker/consumers.d.ts +3 -1
- package/dist/esm/symlinker/consumers.js +7 -1
- package/dist/esm/symlinker/unified-sync.d.ts +3 -3
- package/dist/esm/symlinker/unified-sync.js +88 -11
- package/dist/esm/test/shard-durations.json +0 -2
- 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/cli/commands/init/scaffolders/subagents/index.d.ts +0 -6
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +0 -77
- 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
|
@@ -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,11 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
export declare const AGENT_CONFIG_OXLINT_DIST: string;
|
|
3
|
+
export declare const AGENT_KIT_OXLINT_DIST: string;
|
|
4
|
+
/**
|
|
5
|
+
* Plugin basenames that ship as sibling .js next to oxlintrc.json (not path-roles).
|
|
6
|
+
* SSOT: `@webpresso/agent-config/oxlint/basenames`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VENDOR_PLUGIN_BASENAMES: readonly ["code-safety", "foundation-purity", "graphql-conventions", "import-hygiene", "monorepo-paths", "query-patterns", "testing-quality", "tier-boundaries"];
|
|
9
|
+
export declare function vendorOxlintPlugins(sourceDir?: string, destDir?: string): {
|
|
10
|
+
copied: string[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
import { OXLINT_PATH_ROLES_BASENAME, OXLINT_POLICY_PLUGIN_BASENAMES, } from "@webpresso/agent-config/oxlint/basenames";
|
|
13
|
+
export const AGENT_CONFIG_OXLINT_DIST = join(process.cwd(), "packages/agent-config/dist/esm/oxlint");
|
|
14
|
+
export const AGENT_KIT_OXLINT_DIST = join(process.cwd(), "dist/esm/config/oxlint");
|
|
15
|
+
/**
|
|
16
|
+
* Plugin basenames that ship as sibling .js next to oxlintrc.json (not path-roles).
|
|
17
|
+
* SSOT: `@webpresso/agent-config/oxlint/basenames`.
|
|
18
|
+
*/
|
|
19
|
+
export const VENDOR_PLUGIN_BASENAMES = OXLINT_POLICY_PLUGIN_BASENAMES;
|
|
20
|
+
export function vendorOxlintPlugins(sourceDir = AGENT_CONFIG_OXLINT_DIST, destDir = AGENT_KIT_OXLINT_DIST) {
|
|
21
|
+
if (!existsSync(sourceDir)) {
|
|
22
|
+
throw new Error(`vendor-oxlint-plugins: missing agent-config oxlint dist at ${sourceDir}. Build @webpresso/agent-config first.`);
|
|
23
|
+
}
|
|
24
|
+
mkdirSync(destDir, { recursive: true });
|
|
25
|
+
const copied = [];
|
|
26
|
+
for (const name of VENDOR_PLUGIN_BASENAMES) {
|
|
27
|
+
const src = join(sourceDir, `${name}.js`);
|
|
28
|
+
if (!existsSync(src)) {
|
|
29
|
+
throw new Error(`vendor-oxlint-plugins: missing compiled plugin ${src}`);
|
|
30
|
+
}
|
|
31
|
+
const dest = join(destDir, `${name}.js`);
|
|
32
|
+
cpSync(src, dest);
|
|
33
|
+
copied.push(name);
|
|
34
|
+
// path-roles is imported by several plugins as a sibling — vendor it too.
|
|
35
|
+
}
|
|
36
|
+
// Always vendor path-roles (shared classifier used via relative imports inside plugins).
|
|
37
|
+
const pathRoles = join(sourceDir, `${OXLINT_PATH_ROLES_BASENAME}.js`);
|
|
38
|
+
if (!existsSync(pathRoles)) {
|
|
39
|
+
throw new Error(`vendor-oxlint-plugins: missing ${pathRoles}`);
|
|
40
|
+
}
|
|
41
|
+
cpSync(pathRoles, join(destDir, `${OXLINT_PATH_ROLES_BASENAME}.js`));
|
|
42
|
+
copied.push(OXLINT_PATH_ROLES_BASENAME);
|
|
43
|
+
return { copied };
|
|
44
|
+
}
|
|
45
|
+
if (import.meta.main) {
|
|
46
|
+
const { copied } = vendorOxlintPlugins();
|
|
47
|
+
process.stdout.write(`vendored oxlint plugins: ${copied.join(", ")}\n`);
|
|
48
|
+
}
|
|
@@ -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[];
|
|
@@ -1,35 +1,125 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
4
|
import { dirname, join, resolve } from "node:path";
|
|
6
5
|
import { createRequire } from "node:module";
|
|
7
6
|
import { z } from "zod";
|
|
8
7
|
import { flattenAgentDir, writeFlattenedAssets } from "#compiler/flatten";
|
|
9
|
-
|
|
8
|
+
import { findPackageAsset } from "#utils/package-assets.js";
|
|
9
|
+
import { readPackageManifest, resolvePackageRoot, resolvePackageVersion, } from "#shared-utils/package-root.js";
|
|
10
10
|
const SUPPORTED_TARGETS = ["claude", "codex", "opencode"];
|
|
11
11
|
const DEFAULT_TARGETS = SUPPORTED_TARGETS.join(",");
|
|
12
12
|
const COMPILE_MANIFEST_VERSION = 1;
|
|
13
|
+
/**
|
|
14
|
+
* wp's target names are the stable CLI contract; rulesync's own ids differ for
|
|
15
|
+
* two of the three. rulesync does not reject an unknown target — it logs a skip
|
|
16
|
+
* and exits 0 — so passing wp's names straight through generated nothing for
|
|
17
|
+
* `claude` and `codex` while still reporting success.
|
|
18
|
+
*/
|
|
19
|
+
const RULESYNC_TARGET_IDS = {
|
|
20
|
+
claude: "claudecode",
|
|
21
|
+
codex: "codexcli",
|
|
22
|
+
opencode: "opencode",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* rulesync defaults `--features` to `rules`, which agent-kit never emits — so
|
|
26
|
+
* omitting the flag guaranteed an empty generate. This is the set agent-kit
|
|
27
|
+
* actually flattens.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately not `*`: that also enables `rules`, whose output writes
|
|
30
|
+
* `AGENTS.md`/`CLAUDE.md` at the repo root and collides with the mergeAgentsMd
|
|
31
|
+
* step below, plus `ignore`/`mcp`/`hooks`/`permissions`, whose loaders print
|
|
32
|
+
* ENOENT lines straight to the user's terminal through the inherited stdio.
|
|
33
|
+
*/
|
|
34
|
+
const RULESYNC_FEATURES = "skills,commands,subagents";
|
|
35
|
+
const ASSET_KINDS = ["skills", "commands", "agents"];
|
|
36
|
+
/**
|
|
37
|
+
* Where rulesync writes each asset kind, per wp target.
|
|
38
|
+
*
|
|
39
|
+
* A kind omitted from a target's entry is one rulesync does not emit in project
|
|
40
|
+
* scope — notably codexcli commands, which it supports only under `--global`
|
|
41
|
+
* and otherwise skips with a warning. Omitting it keeps the check below honest
|
|
42
|
+
* rather than demanding a file rulesync was never going to write.
|
|
43
|
+
*/
|
|
44
|
+
const RULESYNC_OUTPUT_PATHS = {
|
|
45
|
+
claude: {
|
|
46
|
+
skills: (name) => join(".claude", "skills", name, "SKILL.md"),
|
|
47
|
+
commands: (name) => join(".claude", "commands", `${name}.md`),
|
|
48
|
+
agents: (name) => join(".claude", "agents", `${name}.md`),
|
|
49
|
+
},
|
|
50
|
+
codex: {
|
|
51
|
+
skills: (name) => join(".agents", "skills", name, "SKILL.md"),
|
|
52
|
+
// Codex subagents are TOML, not markdown — every other target emits .md.
|
|
53
|
+
agents: (name) => join(".codex", "agents", `${name}.toml`),
|
|
54
|
+
},
|
|
55
|
+
opencode: {
|
|
56
|
+
skills: (name) => join(".opencode", "skills", name, "SKILL.md"),
|
|
57
|
+
commands: (name) => join(".opencode", "commands", `${name}.md`),
|
|
58
|
+
agents: (name) => join(".opencode", "agents", `${name}.md`),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
/** Every output path a target is expected to produce for the given assets. */
|
|
62
|
+
function expectedOutputsForTarget(target, assets) {
|
|
63
|
+
const paths = RULESYNC_OUTPUT_PATHS[target];
|
|
64
|
+
const expected = [];
|
|
65
|
+
for (const kind of ASSET_KINDS) {
|
|
66
|
+
const toPath = paths[kind];
|
|
67
|
+
if (!toPath)
|
|
68
|
+
continue;
|
|
69
|
+
for (const name of Object.keys(assets[kind]))
|
|
70
|
+
expected.push(toPath(name));
|
|
71
|
+
}
|
|
72
|
+
return expected;
|
|
73
|
+
}
|
|
74
|
+
function expectedOutputs(targets, assets) {
|
|
75
|
+
return targets.flatMap((target) => expectedOutputsForTarget(target, assets));
|
|
76
|
+
}
|
|
13
77
|
// Resolve rulesync bin — checks consumer's node_modules first,
|
|
14
78
|
// then falls back to webpresso's own bundled rulesync.
|
|
79
|
+
// rulesync's `exports` map declares only ".", so its package.json is not
|
|
80
|
+
// resolvable by subpath — see #shared-utils/package-root for the full trap.
|
|
15
81
|
const _require = createRequire(import.meta.url);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Resolve rulesync relative to agent-kit's installed location rather than to
|
|
84
|
+
* this module's URL.
|
|
85
|
+
*
|
|
86
|
+
* In a bundled-Bun single-file binary `import.meta.url` is a `/$bunfs/root/...`
|
|
87
|
+
* virtual path, so a require anchored on it resolves nothing — and that is
|
|
88
|
+
* exactly the globally-installed case where the consumer has no local rulesync
|
|
89
|
+
* and this fallback is the only copy available.
|
|
90
|
+
*/
|
|
91
|
+
function agentKitAnchoredRequire() {
|
|
92
|
+
const manifestPath = findPackageAsset("package.json");
|
|
93
|
+
if (manifestPath === null)
|
|
94
|
+
return _require;
|
|
95
|
+
return createRequire(join(dirname(manifestPath), "noop.cjs"));
|
|
96
|
+
}
|
|
97
|
+
export function resolveRulesyncBinFromAgentKit(resolver = agentKitAnchoredRequire()) {
|
|
98
|
+
const root = resolvePackageRoot("rulesync", resolver);
|
|
99
|
+
if (!root)
|
|
31
100
|
return null;
|
|
32
|
-
|
|
101
|
+
const binField = readPackageManifest(root)?.bin;
|
|
102
|
+
const rel = typeof binField === "string"
|
|
103
|
+
? binField
|
|
104
|
+
: typeof binField === "object" && binField !== null && "rulesync" in binField
|
|
105
|
+
? String(binField["rulesync"])
|
|
106
|
+
: null;
|
|
107
|
+
if (!rel)
|
|
108
|
+
return null;
|
|
109
|
+
return join(root, rel);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The rulesync version agent-kit itself resolves — the only honest baseline to
|
|
113
|
+
* compare a consumer's install against.
|
|
114
|
+
*
|
|
115
|
+
* This used to be a hand-maintained `PINNED_RULESYNC_VERSION` literal, which
|
|
116
|
+
* drifted five majors behind the declared dependency (9.4.0 vs 14.0.1). The
|
|
117
|
+
* mismatch warning then fired on every single run, including inside agent-kit
|
|
118
|
+
* itself, which trains readers to ignore the one warning meant to catch a real
|
|
119
|
+
* mismatch. Deriving it from the resolved package makes that drift impossible.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveBundledRulesyncVersion(resolver = agentKitAnchoredRequire()) {
|
|
122
|
+
return resolvePackageVersion("rulesync", resolver);
|
|
33
123
|
}
|
|
34
124
|
const CompileManifestSchema = z.object({
|
|
35
125
|
version: z.number(),
|
|
@@ -56,25 +146,6 @@ function readRulesyncVersion(cwd) {
|
|
|
56
146
|
return null;
|
|
57
147
|
}
|
|
58
148
|
}
|
|
59
|
-
function contentHash(assets) {
|
|
60
|
-
const entries = [
|
|
61
|
-
...Object.entries(assets.skills).map(([k, v]) => `s:${k}:${v}`),
|
|
62
|
-
...Object.entries(assets.commands).map(([k, v]) => `c:${k}:${v}`),
|
|
63
|
-
...Object.entries(assets.agents).map(([k, v]) => `a:${k}:${v}`),
|
|
64
|
-
];
|
|
65
|
-
entries.sort();
|
|
66
|
-
return entries.join("\0");
|
|
67
|
-
}
|
|
68
|
-
function readHashFile(p) {
|
|
69
|
-
if (!existsSync(p))
|
|
70
|
-
return null;
|
|
71
|
-
try {
|
|
72
|
-
return readFileSync(p, "utf-8").trim();
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
149
|
/** SHA-256 hash of all .md files under agentDir, recursively (content only). */
|
|
79
150
|
export function hashAgentDir(agentDir) {
|
|
80
151
|
const h = createHash("sha256");
|
|
@@ -144,20 +215,26 @@ export async function runCompile(options) {
|
|
|
144
215
|
}
|
|
145
216
|
const agentDir = join(cwd, ".agent");
|
|
146
217
|
const lockPath = join(agentDir, ".compile.lock");
|
|
147
|
-
const hashPath = join(agentDir, ".compile.hash");
|
|
148
218
|
const manifestPath = join(agentDir, ".compile-manifest.json");
|
|
149
219
|
const rulesyncBin = resolveRulesyncBin(cwd);
|
|
220
|
+
const expectedVersion = resolveBundledRulesyncVersion();
|
|
150
221
|
if (!rulesyncBin || !existsSync(rulesyncBin)) {
|
|
222
|
+
const installHint = expectedVersion ? `rulesync@${expectedVersion}` : "rulesync";
|
|
151
223
|
return {
|
|
152
224
|
ok: false,
|
|
153
225
|
targets: targetList,
|
|
154
226
|
noOp: false,
|
|
155
|
-
message:
|
|
227
|
+
message: `rulesync is not installed — run \`vp add -w ${installHint}\``,
|
|
156
228
|
};
|
|
157
229
|
}
|
|
158
230
|
const installedVersion = readRulesyncVersion(cwd);
|
|
159
|
-
|
|
160
|
-
|
|
231
|
+
// Only compare against a version we actually resolved: with no baseline there
|
|
232
|
+
// is nothing to assert, and a fabricated one is what produced the old
|
|
233
|
+
// permanently-firing warning.
|
|
234
|
+
if (expectedVersion !== null &&
|
|
235
|
+
installedVersion !== null &&
|
|
236
|
+
installedVersion !== expectedVersion) {
|
|
237
|
+
process.stderr.write(`wp compile: warning — installed rulesync@${installedVersion} does not match the rulesync@${expectedVersion} agent-kit resolves\n`);
|
|
161
238
|
}
|
|
162
239
|
// Atomic lock via O_EXCL — fails if another compile is running
|
|
163
240
|
try {
|
|
@@ -171,31 +248,48 @@ export async function runCompile(options) {
|
|
|
171
248
|
message: `wp compile: lock file exists at ${lockPath} — another compile is running`,
|
|
172
249
|
};
|
|
173
250
|
}
|
|
251
|
+
// Staging dir lives in cwd (see the rename below), so an interrupted compile
|
|
252
|
+
// would strand a `.rulesync.tmp-*` directory inside the user's repo that no
|
|
253
|
+
// later run reaps. Tracked here so the signal path clears it too.
|
|
254
|
+
let stagingDir = null;
|
|
174
255
|
const cleanup = () => {
|
|
175
256
|
try {
|
|
176
257
|
if (existsSync(lockPath))
|
|
177
258
|
unlinkSync(lockPath);
|
|
259
|
+
if (stagingDir !== null)
|
|
260
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
178
261
|
}
|
|
179
262
|
catch {
|
|
180
263
|
/* best-effort */
|
|
181
264
|
}
|
|
182
265
|
};
|
|
183
|
-
|
|
184
|
-
process
|
|
266
|
+
// Held as named refs so `finally` can detach them: runCompile is called
|
|
267
|
+
// repeatedly within one process (the test suites do exactly this), and
|
|
268
|
+
// anonymous handlers would stack up a MaxListenersExceededWarning plus N
|
|
269
|
+
// SIGINT handlers each racing to call process.exit.
|
|
270
|
+
const onSigint = () => {
|
|
185
271
|
cleanup();
|
|
186
272
|
process.exit(130);
|
|
187
|
-
}
|
|
188
|
-
|
|
273
|
+
};
|
|
274
|
+
const onSigterm = () => {
|
|
189
275
|
cleanup();
|
|
190
276
|
process.exit(143);
|
|
191
|
-
}
|
|
277
|
+
};
|
|
278
|
+
process.on("exit", cleanup);
|
|
279
|
+
process.on("SIGINT", onSigint);
|
|
280
|
+
process.on("SIGTERM", onSigterm);
|
|
192
281
|
try {
|
|
193
282
|
const assets = flattenAgentDir(agentDir);
|
|
194
|
-
const hash = contentHash(assets);
|
|
195
283
|
const sourceHash = hashAgentDir(agentDir);
|
|
196
|
-
//
|
|
284
|
+
// An unchanged source hash only proves the *input* is the same; it says
|
|
285
|
+
// nothing about whether the generated surfaces still exist. Deleting
|
|
286
|
+
// .claude/skills/ would otherwise leave compile reporting "no changes"
|
|
287
|
+
// forever while the files it exists to produce stay missing — the very
|
|
288
|
+
// silent-zero-output failure this command was repaired to eliminate.
|
|
289
|
+
const expected = expectedOutputs(targetList, assets);
|
|
290
|
+
const outputsPresent = expected.every((rel) => existsSync(join(cwd, rel)));
|
|
197
291
|
const existingManifest = readCompileManifest(manifestPath);
|
|
198
|
-
if (existingManifest !== null && existingManifest.sourceHash === sourceHash) {
|
|
292
|
+
if (outputsPresent && existingManifest !== null && existingManifest.sourceHash === sourceHash) {
|
|
199
293
|
return {
|
|
200
294
|
ok: true,
|
|
201
295
|
targets: targetList,
|
|
@@ -203,30 +297,35 @@ export async function runCompile(options) {
|
|
|
203
297
|
message: "No changes (manifest matches)",
|
|
204
298
|
};
|
|
205
299
|
}
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const tmpOut = mkdtempSync(join(tmpdir(), "wp-compile-"));
|
|
300
|
+
// Step 1+2: Stage the flattened assets, then atomically rename to .rulesync/.
|
|
301
|
+
//
|
|
302
|
+
// The staging dir must live inside `cwd`, not in os.tmpdir(): renameSync
|
|
303
|
+
// cannot cross filesystems, and on any host where /tmp is its own mount
|
|
304
|
+
// (tmpfs on most systemd Linux distros, Docker --tmpfs, many CI runners)
|
|
305
|
+
// the swap below would throw EXDEV and take the whole command down. The
|
|
306
|
+
// test suites cannot catch that on their own, because their cwd is itself
|
|
307
|
+
// under tmpdir() and so always lands on the same filesystem.
|
|
308
|
+
const tmpOut = mkdtempSync(join(cwd, ".rulesync.tmp-"));
|
|
309
|
+
stagingDir = tmpOut;
|
|
217
310
|
try {
|
|
218
311
|
await writeFlattenedAssets(assets, tmpOut);
|
|
219
312
|
const rulesyncInputDir = join(cwd, ".rulesync");
|
|
220
313
|
if (existsSync(rulesyncInputDir))
|
|
221
314
|
rmSync(rulesyncInputDir, { recursive: true, force: true });
|
|
222
315
|
renameSync(tmpOut, rulesyncInputDir);
|
|
316
|
+
// Renamed away: nothing left at the staging path for cleanup to remove.
|
|
317
|
+
stagingDir = null;
|
|
223
318
|
}
|
|
224
319
|
catch (err) {
|
|
225
320
|
rmSync(tmpOut, { recursive: true, force: true });
|
|
321
|
+
stagingDir = null;
|
|
226
322
|
throw err;
|
|
227
323
|
}
|
|
228
324
|
// Step 3: Run rulesync generate
|
|
229
|
-
const
|
|
325
|
+
const rulesyncTargets = targetList
|
|
326
|
+
.map((target) => RULESYNC_TARGET_IDS[target])
|
|
327
|
+
.join(",");
|
|
328
|
+
const result = spawnSync(rulesyncBin, ["generate", "--targets", rulesyncTargets, "--features", RULESYNC_FEATURES], {
|
|
230
329
|
cwd,
|
|
231
330
|
stdio: "inherit",
|
|
232
331
|
});
|
|
@@ -247,6 +346,21 @@ export async function runCompile(options) {
|
|
|
247
346
|
message: `wp compile: rulesync exited with code ${exitCode}`,
|
|
248
347
|
};
|
|
249
348
|
}
|
|
349
|
+
// rulesync exits 0 when it generates nothing at all ("All files are up to
|
|
350
|
+
// date"), so the exit code above cannot distinguish a real generate from a
|
|
351
|
+
// silent no-op caused by a target it skipped or input it could not see.
|
|
352
|
+
// The files consumers need are the contract, so assert they exist.
|
|
353
|
+
const missing = expected.filter((rel) => !existsSync(join(cwd, rel)));
|
|
354
|
+
if (missing.length > 0) {
|
|
355
|
+
const shown = missing.slice(0, 5).join(", ");
|
|
356
|
+
const rest = missing.length > 5 ? ` (and ${missing.length - 5} more)` : "";
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
targets: targetList,
|
|
360
|
+
noOp: false,
|
|
361
|
+
message: `wp compile: rulesync exited 0 but ${missing.length} expected output(s) were not generated: ${shown}${rest}`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
250
364
|
// Step 4: Run mergeAgentsMd to produce AGENTS.md at repo root
|
|
251
365
|
// (Plugin manifests are hand-maintained SSOT under packages/*-plugin/;
|
|
252
366
|
// wp compile no longer emits them — golden tests + version stamping own
|
|
@@ -270,6 +384,10 @@ export async function runCompile(options) {
|
|
|
270
384
|
const outputHashes = {
|
|
271
385
|
"AGENTS.md": hashFile(join(cwd, "AGENTS.md")),
|
|
272
386
|
};
|
|
387
|
+
// Every generated surface, not just AGENTS.md — this is what gives
|
|
388
|
+
// `wp audit compile-drift` something real to compare against.
|
|
389
|
+
for (const rel of expected)
|
|
390
|
+
outputHashes[rel] = hashFile(join(cwd, rel));
|
|
273
391
|
const compileManifest = {
|
|
274
392
|
version: COMPILE_MANIFEST_VERSION,
|
|
275
393
|
timestamp: new Date().toISOString(),
|
|
@@ -277,7 +395,6 @@ export async function runCompile(options) {
|
|
|
277
395
|
outputHashes,
|
|
278
396
|
};
|
|
279
397
|
writeCompileManifest(manifestPath, compileManifest);
|
|
280
|
-
writeFileSync(hashPath, hash);
|
|
281
398
|
return {
|
|
282
399
|
ok: true,
|
|
283
400
|
targets: targetList,
|
|
@@ -287,6 +404,9 @@ export async function runCompile(options) {
|
|
|
287
404
|
}
|
|
288
405
|
finally {
|
|
289
406
|
cleanup();
|
|
407
|
+
process.removeListener("exit", cleanup);
|
|
408
|
+
process.removeListener("SIGINT", onSigint);
|
|
409
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
290
410
|
}
|
|
291
411
|
}
|
|
292
412
|
function collectAgentsLayers(agentDir) {
|
|
@@ -178,11 +178,27 @@ function formatterTargetFromCwd(file, cwd) {
|
|
|
178
178
|
return file;
|
|
179
179
|
return relativeTarget;
|
|
180
180
|
}
|
|
181
|
+
// The blueprint root is not always the top-level `blueprints/` directory: a
|
|
182
|
+
// consumer repo can resolve to `webpresso/blueprints/` (the webpresso-legacy
|
|
183
|
+
// fallback layout — see #utils/blueprint-root.js) or a `.webpressorc.json`
|
|
184
|
+
// override. A prefix anchored to one specific root silently fails to exclude
|
|
185
|
+
// review-artifact markdown under any other layout, so `wp format` reformats
|
|
186
|
+
// it. That mutates the artifact's bytes between when `wp review gate`
|
|
187
|
+
// records its sha256 into review-events.jsonl and when the file is
|
|
188
|
+
// committed, producing a false "Review artifact digest mismatch" at
|
|
189
|
+
// `wp_blueprint_finalize` time (src/review/authority.ts correctly detects
|
|
190
|
+
// the drift and fails closed). Match on the directory name alone — not
|
|
191
|
+
// anchored to any specific root prefix — so this stays correct across every
|
|
192
|
+
// current and future blueprint root layout without tracking
|
|
193
|
+
// resolveBlueprintRoot's precedence rules in a second place. This also keeps
|
|
194
|
+
// parity with the whole-repo formatter's own ignore layer
|
|
195
|
+
// (vite.config.ts#fmt.ignorePatterns), which uses the same unanchored
|
|
196
|
+
// `**/review-artifacts/**` glob for the same reason.
|
|
181
197
|
function filterFormatableFiles(files, cwd) {
|
|
182
198
|
return files.filter((file) => {
|
|
183
199
|
const extension = path.extname(file).toLowerCase();
|
|
184
200
|
const normalized = file.replaceAll("\\", "/");
|
|
185
|
-
const isReviewArtifact = normalized.
|
|
201
|
+
const isReviewArtifact = normalized.includes("/review-artifacts/");
|
|
186
202
|
return OXFMT_EXTENSIONS.has(extension) && !isReviewArtifact && existsSync(path.join(cwd, file));
|
|
187
203
|
});
|
|
188
204
|
}
|