@webpieces/rules-config 0.4.627 → 0.4.629
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/package.json +1 -1
- package/src/constants.js +4 -3
- package/src/constants.js.map +1 -1
- package/src/review-json.js +6 -3
- package/src/review-json.js.map +1 -1
- package/src/state-dir-migration.d.ts +19 -5
- package/src/state-dir-migration.js +24 -8
- package/src/state-dir-migration.js.map +1 -1
- package/src/state-dir.d.ts +47 -6
- package/src/state-dir.js +62 -8
- package/src/state-dir.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.629",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/constants.js
CHANGED
|
@@ -48,9 +48,10 @@ exports.RULE_NAMES = {
|
|
|
48
48
|
// `.webpieces/` is gitignored; only the per-feature subdirs under those two homes are subject
|
|
49
49
|
// to 30-day cleanup (the homes themselves, like hooks/ and instruct-ai/, are permanent).
|
|
50
50
|
// The DIRECTORY NAME only. Never join it onto a root yourself — go through `DotWebpieces.shared()`
|
|
51
|
-
// (repo-wide state)
|
|
52
|
-
//
|
|
53
|
-
//
|
|
51
|
+
// (repo-wide state), `DotWebpieces.local()` (this worktree's own tooling-written state) or
|
|
52
|
+
// `DotWebpieces.aiWritable()` (this worktree's state that a CODING AGENT writes) so the call site
|
|
53
|
+
// declares its scope. In a linked worktree the three resolve to different places, and getting that
|
|
54
|
+
// silently wrong is the bug those methods exist to prevent.
|
|
54
55
|
exports.WEBPIECES_TMP_DIR = '.webpieces';
|
|
55
56
|
exports.MERGE_INFO_DIR = 'merge-info';
|
|
56
57
|
// The PR working home. Renamed from the legacy `pr-info` to `pr-review` for clarity (it holds the
|
package/src/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/constants.ts"],"names":[],"mappings":";AAAA,iFAAiF;AACjF,sEAAsE;AACtE,EAAE;AACF,yFAAyF;AACzF,qFAAqF;AACrF,kFAAkF;;;
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/constants.ts"],"names":[],"mappings":";AAAA,iFAAiF;AACjF,sEAAsE;AACtE,EAAE;AACF,yFAAyF;AACzF,qFAAqF;AACrF,kFAAkF;;;AA8FlF,gCAEC;AA9FY,QAAA,iBAAiB,GAAG,mBAAmB,CAAC;AAErD,kFAAkF;AAClF,uFAAuF;AACvF,uEAAuE;AACvE,sFAAsF;AACzE,QAAA,UAAU,GAAG;IACtB,cAAc,EAAE,gBAAgB;IAChC,eAAe,EAAE,iBAAiB;IAClC,cAAc,EAAE,gBAAgB;IAChC,uBAAuB,EAAE,yBAAyB;IAClD,mBAAmB,EAAE,qBAAqB;IAC1C,oBAAoB,EAAE,sBAAsB;IAC5C,mBAAmB,EAAE,qBAAqB;IAC1C,mBAAmB,EAAE,qBAAqB;IAC1C,2CAA2C,EAAE,6CAA6C;IAC1F,4BAA4B,EAAE,8BAA8B;IAC5D,yBAAyB,EAAE,2BAA2B;IACtD,+CAA+C,EAAE,iDAAiD;IAClG,aAAa,EAAE,eAAe;IAC9B,QAAQ,EAAE,UAAU;IACpB,eAAe,EAAE,iBAAiB;IAClC,sBAAsB,EAAE,wBAAwB;IAChD,aAAa,EAAE,eAAe;IAC9B,gBAAgB,EAAE,kBAAkB;IACpC,qBAAqB,EAAE,uBAAuB;IAC9C,wBAAwB,EAAE,0BAA0B;IACpD,kBAAkB,EAAE,oBAAoB;CAClC,CAAC;AAEX,wFAAwF;AACxF,0FAA0F;AAC1F,sFAAsF;AACtF,0DAA0D;AAC1D,EAAE;AACF,mFAAmF;AACnF,uFAAuF;AACvF,0FAA0F;AAC1F,iGAAiG;AACjG,8FAA8F;AAC9F,yFAAyF;AACzF,mGAAmG;AACnG,2FAA2F;AAC3F,kGAAkG;AAClG,mGAAmG;AACnG,4DAA4D;AAC/C,QAAA,iBAAiB,GAAG,YAAY,CAAC;AACjC,QAAA,cAAc,GAAG,YAAY,CAAC;AAC3C,kGAAkG;AAClG,qGAAqG;AACrG,iCAAiC;AACpB,QAAA,aAAa,GAAG,WAAW,CAAC;AAC5B,QAAA,sBAAsB,GAAG,wBAAwB,CAAC;AAE/D,uFAAuF;AACvF,qGAAqG;AACrG,8FAA8F;AAC9F,2DAA2D;AAC3D,EAAE;AACF,kGAAkG;AAClG,iGAAiG;AACjG,oGAAoG;AACpG,kGAAkG;AACrF,QAAA,mBAAmB,GAAG,2BAA2B,CAAC;AAE/D,2FAA2F;AAC3F,mFAAmF;AACnF,uFAAuF;AACvF,uGAAuG;AACvG,kGAAkG;AAClG,kGAAkG;AAClG,iDAAiD;AACpC,QAAA,sBAAsB,GAAG,sBAAsB,CAAC;AAE7D;;;;;;;;;GASG;AACU,QAAA,qBAAqB,GAAG,8BAA8B,CAAC;AAEpE;;;;;GAKG;AACH,SAAgB,UAAU,CAAC,IAAY,EAAE,QAAgB;IACrD,OAAO,IAAI,CAAC,QAAQ,CAAC,yBAAiB,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACvE,CAAC","sourcesContent":["// Single source of truth for the disable-comment token and rule-name identifiers\n// shared across rules-config, ai-hook-rules, code-rules, and pr-gate.\n//\n// There is exactly ONE disable form: `// webpieces-disable <rule>[, <rule2>] -- reason`.\n// The legacy `ai-hook-disable` alias and the `-file`/`-next`/`-all` variants and the\n// `*`/bare (no-rule) wildcard have been removed — every disable MUST name a rule.\n\nexport const WEBPIECES_DISABLE = 'webpieces-disable';\n\n// Rule-name tokens as they appear AFTER `webpieces-disable` in a disable comment.\n// Values must match existing comments exactly — changing a value silently breaks every\n// disable that names that rule. Note MAX_LINES_MODIFIED is a prefix of\n// MAX_LINES_MODIFIED_FILES (a historical substring-match quirk preserved on purpose).\nexport const RULE_NAMES = {\n NO_ANY_UNKNOWN: 'no-any-unknown',\n NO_IMPLICIT_ANY: 'no-implicit-any',\n NO_DESTRUCTURE: 'no-destructure',\n NO_UNMANAGED_EXCEPTIONS: 'no-unmanaged-exceptions',\n CATCH_ERROR_PATTERN: 'catch-error-pattern',\n THROW_CAUSE_REQUIRED: 'throw-cause-required',\n REQUIRE_RETURN_TYPE: 'require-return-type',\n NO_SYMBOL_DI_TOKENS: 'no-symbol-di-tokens',\n NO_CLIENT_CREATION_OUTSIDE_SERVER_OR_CLIENT: 'no-client-creation-outside-server-or-client',\n NO_PROCESS_EXIT_OUTSIDE_MAIN: 'no-process-exit-outside-main',\n NO_FUNCTION_OUTSIDE_CLASS: 'no-function-outside-class',\n INJECT_ANNOTATION_NOT_NEEDED_FOR_CONCRETE_CLASS: 'inject-annotation-not-needed-for-concrete-class',\n FRAMEWORK_TAG: 'framework-tag',\n ROLE_TAG: 'role-tag',\n NO_INLINE_TYPES: 'no-inline-types',\n NO_DIRECT_API_RESOLVER: 'no-direct-api-resolver',\n NO_CUSTOM_CSS: 'no-custom-css',\n PRISMA_CONVERTER: 'prisma-converter',\n MAX_LINES_NEW_METHODS: 'max-lines-new-methods',\n MAX_LINES_MODIFIED_FILES: 'max-lines-modified-files',\n MAX_LINES_MODIFIED: 'max-lines-modified',\n} as const;\n\n// Merge-state convention shared by the pr-gate scripts (which WRITE the marker during a\n// conflicted 3-point merge) and the ai-hook-rules merge-in-progress-guard (which READS it\n// to block commit/push/PR until the merge is validated). Kept here so neither package\n// depends on the other — they only share this vocabulary.\n//\n// `.webpieces/` is the single working dir for all webpieces tooling: ai-hook-rules\n// bootstrap/cache, the instruct-ai docs, and the workflow state. To keep the top level\n// quiet, per-feature workflow dirs are nested one level down under `merge-info/<feature>`\n// and `pr-review/<feature>` rather than scattered as top-level `merge-<feature>`/`pr-<feature>`.\n// `.webpieces/` is gitignored; only the per-feature subdirs under those two homes are subject\n// to 30-day cleanup (the homes themselves, like hooks/ and instruct-ai/, are permanent).\n// The DIRECTORY NAME only. Never join it onto a root yourself — go through `DotWebpieces.shared()`\n// (repo-wide state), `DotWebpieces.local()` (this worktree's own tooling-written state) or\n// `DotWebpieces.aiWritable()` (this worktree's state that a CODING AGENT writes) so the call site\n// declares its scope. In a linked worktree the three resolve to different places, and getting that\n// silently wrong is the bug those methods exist to prevent.\nexport const WEBPIECES_TMP_DIR = '.webpieces';\nexport const MERGE_INFO_DIR = 'merge-info';\n// The PR working home. Renamed from the legacy `pr-info` to `pr-review` for clarity (it holds the\n// AI's PR review + rendered body). Old `pr-info/` dirs are gitignored local state and self-clear via\n// cleanTmp's legacy `pr-` sweep.\nexport const PR_REVIEW_DIR = 'pr-review';\nexport const MERGE_IN_PROGRESS_FILE = 'merge-in-progress.json';\n\n// The dev-deploy resolve state file, written by `wp-push-dev --resolve` and cleared by\n// `wp-finish-push-dev` (or `--abort`). Named here for the same reason MERGE_IN_PROGRESS_FILE is: the\n// pr-gate commands WRITE it and things outside pr-gate READ it to decide whether a resolve is\n// half-finished, and neither side may depend on the other.\n//\n// It lives directly under `.webpieces/` local state (NOT under merge-info/), because a dev-deploy\n// resolve is not a 3-point merge: it never touches the feature branch, never produces merge-info\n// context, and must NOT be picked up by merge-in-progress-guard's marker scan — that guard's remedy\n// is `pnpm wp-finish-upsert-pr`, which is the wrong command here and would strand the tmp branch.\nexport const PUSH_DEV_STATE_FILE = 'push-dev-in-progress.json';\n\n// Proof-of-work the AI must produce for every conflicted file it resolves during a 3-point\n// merge: a short explanation written NEXT TO that file's 3-point context (the same\n// `updatemain-<safe_path>/` dir that holds A-forkpoint.txt / B-A.diff / C-A.diff). The\n// wp-finish-upsert-pr gate requires a non-empty file of this name per conflicted file before passing —\n// it is the only check on the part of the process the AI actually owns (resolving files). Using a\n// sidecar file (rather than an in-source comment) works for any file type, including comment-less\n// ones like JSON and files resolved by deletion.\nexport const MERGE_EXPLANATION_FILE = 'merge-explanation.md';\n\n/**\n * The command that MECHANICALLY strips every unknown key from webpieces.config.json.\n *\n * An unknown key controls nothing, so deleting it is the cure — but \"delete it\" is a judgement call made\n * while the guard denies every Bash call, which is exactly when a reader is least able to make one. This\n * command turns that judgement into one keystroke, and every message that reports an unknown key names it.\n *\n * Lives in this leaf module (no imports) so both the banner and the validators can name it without\n * importing the pruner and re-creating an import cycle.\n */\nexport const PRUNE_UNKNOWN_COMMAND = 'pnpm wp-prune-unknown-config';\n\n/**\n * Fast predicate: does this text carry a webpieces-disable for the given rule?\n * Line-agnostic — the caller decides which line(s) or block of text to feed it.\n * This is the cheap substring form used by code-rules detection and pr-gate's\n * dashboard grep/count. (ai-hook-rules uses a richer line-mapping parser.)\n */\nexport function hasDisable(text: string, ruleName: string): boolean {\n return text.includes(WEBPIECES_DISABLE) && text.includes(ruleName);\n}\n"]}
|
package/src/review-json.js
CHANGED
|
@@ -57,10 +57,13 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
57
57
|
constructor(dotDir = state_dir_1.dotWebpieces) {
|
|
58
58
|
this.dotDir = dotDir;
|
|
59
59
|
}
|
|
60
|
-
// The per-feature PR working dir: `<
|
|
61
|
-
//
|
|
60
|
+
// The per-feature PR working dir: `<worktree>/.webpieces/pr-review/<feature>`. AI-WRITABLE scope,
|
|
61
|
+
// not local() — an agent AUTHORS review.json here, and each reviewer subagent authors its own
|
|
62
|
+
// review-<id>.json beside it. A worktree-isolated agent's Write is refused for any path under the
|
|
63
|
+
// shared checkout, which is where local() puts this, so local() made both files unwritable by the
|
|
64
|
+
// very agents the flow instructs to write them. See DotWebpieces.aiWritable() for the full account.
|
|
62
65
|
prDirFor(repoRoot, featureName) {
|
|
63
|
-
return this.dotDir.
|
|
66
|
+
return this.dotDir.aiWritableFile(repoRoot, constants_1.PR_REVIEW_DIR, featureName);
|
|
64
67
|
}
|
|
65
68
|
// Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.
|
|
66
69
|
reviewJsonPath(repoRoot, featureName) {
|
package/src/review-json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { PR_REVIEW_DIR } from './constants';\nimport { DotWebpieces, dotWebpieces } from './state-dir';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\nimport {\n VERDICT_GREEN,\n VERDICT_YELLOW,\n VERDICT_RED,\n VERDICT_STATUSES,\n ChecklistResult,\n RequiredChecklist,\n ChecklistReviewContext,\n ReviewJson,\n CK_PASS,\n CK_WARN,\n CK_OVERRIDDEN,\n CK_FAIL,\n CK_MISSING,\n CK_BAD_FORMAT,\n ChecklistVerdict,\n PrContext,\n} from './review-json-data';\n\n// Re-exported so review-json.ts stays the single import site for the whole review vocabulary: the data\n// classes moved out to keep this file under the file-size limit, NOT to give callers a second module to\n// learn. Every existing `from './review-json'` import keeps resolving.\nexport {\n VERDICT_GREEN,\n VERDICT_YELLOW,\n VERDICT_RED,\n VERDICT_STATUSES,\n ChecklistResult,\n RequiredChecklist,\n ChecklistReviewContext,\n ReviewJson,\n CK_PASS,\n CK_WARN,\n CK_OVERRIDDEN,\n CK_FAIL,\n CK_MISSING,\n CK_BAD_FORMAT,\n ChecklistVerdict,\n PrContext,\n};\n\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n// Where `wp-finish-upsert-pr` retires the review.json it just consumed, and the note it stamps on the way.\n// The key sorts first in the written JSON because it is written first — an AI that opens the file to see\n// whether it can reuse the review reads what the file IS before it reads a title it might be tempted to keep.\nconst OLD_REVIEW_FILE = 'old-review.json';\nconst ARCHIVE_NOTE_KEY = '_ARCHIVED_AUDIT_ONLY';\nconst ARCHIVE_NOTE =\n 'ARCHIVE — this is the review from the PREVIOUS wp-finish-upsert-pr run on this branch, kept for audit ' +\n 'purposes only. It is NOT the review for a new review round: it describes the code as of the last PR ' +\n 'update, which has since moved. If you are reviewing again, write a FRESH review.json at the path ' +\n 'pnpm wp-review-upsert-pr prints; do not copy this file\\'s title, summary or risk level forward without ' +\n 're-deciding each one. Overwritten by every finish, so only the most recent review is ever here.';\n\n// The same stamp, for a retired per-checklist verdict. Verdict files get their OWN wording because the two\n// archives answer different questions: old-review.json holds a description of the code, this holds a\n// REVIEWER'S DECISION. The one thing that must not happen is a reader treating an archived red as the live\n// verdict — the whole reason the file was moved rather than copied — so the note says that outright.\nconst CHECKLIST_ARCHIVE_NOTE =\n 'ARCHIVE — this is a checklist verdict from a PREVIOUS reviewer run on this branch, kept for audit ' +\n 'purposes only. It is NOT a live verdict and must never be read back as one: it was RETIRED because it ' +\n 'refused the PR, and the gate moved it here so the only way forward is a FRESH review-<id>.json written ' +\n 'by a real reviewer run. Do not copy its status back onto the live path to get past the gate. ' +\n 'Overwritten by every retirement, so only the most recently retired verdict is ever here.';\n\n/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewJsonService {\n constructor(private readonly dotDir: DotWebpieces = dotWebpieces) {}\n\n // The per-feature PR working dir: `<local .webpieces>/pr-review/<feature>`. LOCAL scope — a PR\n // review belongs to the worktree that is preparing it, and two worktrees never share one.\n prDirFor(repoRoot: string, featureName: string): string {\n return this.dotDir.localFile(repoRoot, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Absolute path of the pr-context.json for a feature (the diff base/head + changed files).\n prContextPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'pr-context.json');\n }\n\n // Where a consumed review.json is archived to, beside it. Always the SAME path — it holds the last\n // review and only the last one, so it can never be mistaken for a series that means something.\n oldReviewJsonPath(reviewJsonFilePath: string): string {\n return path.join(path.dirname(reviewJsonFilePath), OLD_REVIEW_FILE);\n }\n\n /**\n * Retire the review `wp-finish-upsert-pr` just used: move review.json to old-review.json, stamped with a\n * note saying what it is. Returns the archive path, or '' when there was nothing to archive.\n *\n * The point is the MOVE, not the copy. review.json left in place after a PR is posted is a live-looking\n * file describing a review that already happened, and the next run of stage ② on this branch finds it\n * sitting there — so a reviewer subagent that judges the PR's stated intent (its title, summary or risk\n * level) can read the previous run's review and return GREEN against a title that no longer exists.\n * Nothing in the verdict distinguishes that from a real pass. Moving it means the only way to reach\n * finish again is to write a fresh one, and {@link loadReviewJson} points at the archive when it is\n * missing so the archive reads as an audit trail rather than as a lost file.\n *\n * Called only after the PR is actually up: a finish that failed before publishing must stay re-runnable.\n */\n archiveReviewJson(reviewJsonFilePath: string): string {\n if (!fs.existsSync(reviewJsonFilePath)) return '';\n const archivePath = this.oldReviewJsonPath(reviewJsonFilePath);\n const raw = fs.readFileSync(reviewJsonFilePath, 'utf8');\n fs.writeFileSync(archivePath, this.archivedBody(raw, ARCHIVE_NOTE));\n fs.rmSync(reviewJsonFilePath);\n return archivePath;\n }\n\n /**\n * The archived bytes: the original JSON with an AUDIT-ONLY note as its FIRST key, so anything that opens\n * the file — human or AI — reads what it is before it reads any of its content.\n *\n * `note` is a parameter rather than a constant because two different files are archived here (review.json\n * and review-<id>.json) and they need to say different things, while the stamping MECHANICS — parse,\n * note first, original keys in order, fall back to raw — are identical. One implementation, two texts;\n * a second copy of this method would be the thing that drifts.\n *\n * Falls back to the raw bytes when they do not parse. For review.json `loadReviewJson` has already\n * accepted the file so that is close to impossible, but a verdict file is written by a subagent and may\n * be half-written or not an object at all — and preserving the original always beats losing it to a\n * stamping failure, since the archive exists precisely to be the record.\n */\n private archivedBody(raw: string, note: string): string {\n const parsed = this.tryParseObject(raw);\n if (parsed === null) return raw;\n // webpieces-disable no-any-unknown -- re-serializing opaque review fields verbatim; only the key ORDER is ours\n const stamped: Record<string, unknown> = {};\n stamped[ARCHIVE_NOTE_KEY] = note;\n for (const key of Object.keys(parsed)) stamped[key] = parsed[key];\n return JSON.stringify(stamped, null, 2) + '\\n';\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON; the caller only re-serializes it\n private tryParseObject(raw: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unparseable bytes are archived verbatim, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque; only its key order is used\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null;\n return parsed;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * Persist the PR's diff context so reviewer subagents can read the changed-file set + the exact base\n * sha. Returns the file path written.\n *\n * ALSO writes an immutable per-stage snapshot under `stages/<stage>.json` when `stage` is given.\n * `pr-context.json` is overwritten by each stage, so by the time anything goes wrong the earlier\n * states are gone — and \"what did the tooling think the diff was at stage ①, vs ②, vs ③?\" is exactly\n * the question you need answered when debugging a review that went sideways. The snapshots make the\n * review system auditable by an AI reviewing IT, which the single mutable file never could.\n */\n writePrContext(repoRoot: string, featureName: string, context: PrContext, stage = ''): string {\n const dir = this.prDirFor(repoRoot, featureName);\n fs.mkdirSync(dir, { recursive: true });\n const body = JSON.stringify(context, null, 2) + '\\n';\n const p = this.prContextPath(repoRoot, featureName);\n fs.writeFileSync(p, body);\n if (stage !== '') {\n const stagesDir = path.join(dir, 'stages');\n fs.mkdirSync(stagesDir, { recursive: true });\n fs.writeFileSync(path.join(stagesDir, `${stage}.json`), body);\n }\n return p;\n }\n\n /**\n * The review context for a feature, recovered from the pr-context.json wp-start-upsert-pr already wrote.\n * Lets wp-finish-upsert-pr's \"you still owe me review-<id>.json\" message inline the SAME self-sufficient\n * per-reviewer block start printed, instead of a checklist name and an indirection. Empty when the file\n * is absent or unreadable — the block then just omits those lines.\n */\n reviewContextFor(repoRoot: string, featureName: string): ChecklistReviewContext {\n const p = this.prContextPath(repoRoot, featureName);\n if (!fs.existsSync(p)) return new ChecklistReviewContext();\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context file degrades to fewer printed lines, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed on the next line\n const raw = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;\n const base = typeof raw['base'] === 'string' ? (raw['base'] as string) : '';\n // Recover the REPRODUCE command rather than re-deriving it: a context written by an older\n // pr-gate has no diffCommand, and guessing `<base> HEAD` there would resurrect the exact\n // empty-on-a-dirty-tree bug this field exists to kill. Absent ⇒ omit the line entirely.\n const cmd = typeof raw['diffCommand'] === 'string' ? (raw['diffCommand'] as string) : '';\n const diffDir = typeof raw['diffDir'] === 'string' ? (raw['diffDir'] as string) : '';\n const dirty = raw['dirty'] === true;\n return new ChecklistReviewContext(base, p, cmd === '' ? '' : `${cmd} -- <file>`, diffDir, dirty);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return new ChecklistReviewContext('', p);\n }\n }\n\n // Copy-paste schema both commands print. `required` is the set of checklists the diff MATCHED; empty\n // ⇒ output identical to a repo with no checklists. Non-empty ⇒ appends per-checklist instructions\n // naming the reviewer subagent + doc + the review-<id>.json to write.\n reviewJsonSchemaHint(filePath: string): string {\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]\\n` +\n `}`\n );\n }\n\n /**\n * The extra line the \"no review.json\" complaint carries when a PREVIOUS review was archived here. It\n * turns a bare \"not found\" — which reads as data loss, and invites hunting for the file — into the fact:\n * the last finish consumed it, and the archive is audit material, not a review to reuse.\n */\n private archivedReviewHint(filePath: string): string {\n const archive = this.oldReviewJsonPath(filePath);\n if (!fs.existsSync(archive)) return '';\n return `\\nA PREVIOUS review was archived to ${archive} when the last pnpm wp-finish-upsert-pr consumed it.\\n` +\n `That file is for AUDIT ONLY — it reviews code this branch has since moved past. Write a fresh one:`;\n }\n\n // The per-checklist review file path that sits beside review.json: review-<id>.json.\n checklistResultPath(reviewJsonFilePath: string, checklistId: string): string {\n return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);\n }\n\n /**\n * Where a RETIRED verdict for one checklist goes: `review-<id>.json.old`, beside the live path.\n *\n * Mirrors {@link oldReviewJsonPath} deliberately, including its single-slot rule: ALWAYS the same path,\n * so it holds the last retired verdict and only the last one. A series (`.old.old`, `.old.1`) would read\n * as though the number of retirements meant something, and nothing downstream can interpret that — the\n * one fact worth keeping is \"this checklist refused before, here is what it said\".\n */\n oldChecklistResultPath(reviewJsonFilePath: string, checklistId: string): string {\n return `${this.checklistResultPath(reviewJsonFilePath, checklistId)}.old`;\n }\n\n /**\n * Retire one checklist's verdict: MOVE review-<id>.json to review-<id>.json.old, stamped with a note\n * saying what it is. Returns the archive path, or '' when there was nothing to archive.\n *\n * The point is the MOVE, exactly as in {@link archiveReviewJson}. A red verdict left on the live path is\n * re-read by the next run and re-reported as the CURRENT state of the branch, so the branch keeps being\n * refused for a finding that may already be fixed — and the fix, when it comes, silently overwrites the\n * only record that the gate ever refused anything. Moving it makes the refusal durable and makes a fresh\n * reviewer run the only way forward, which is the honest requirement: the old verdict judged code that\n * has since changed.\n *\n * Safe by construction for RED verdicts specifically, which is why the caller only archives on CK_FAIL:\n * a red verdict is never reusable — it always blocks — so nothing is lost by moving it. Green and yellow\n * verdicts ARE deliberately reused across finish attempts, and retiring one would force a needless (and\n * expensive) subagent re-run.\n */\n archiveChecklistResult(reviewJsonFilePath: string, checklistId: string): string {\n const livePath = this.checklistResultPath(reviewJsonFilePath, checklistId);\n if (!fs.existsSync(livePath)) return '';\n const archivePath = this.oldChecklistResultPath(reviewJsonFilePath, checklistId);\n const raw = fs.readFileSync(livePath, 'utf8');\n fs.writeFileSync(archivePath, this.archivedBody(raw, CHECKLIST_ARCHIVE_NOTE));\n fs.rmSync(livePath);\n return archivePath;\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one\n * must have a well-formed, passing (or overridden) review-<id>.json or a validation error is raised\n * alongside the usual ones so the AI gets ONE message.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.${this.archivedReviewHint(filePath)}\\n\\n` +\n `${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n const results = this.loadChecklistResults(filePath, required);\n for (const err of this.requiredChecklistErrors(required, results)) errors.push(err);\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n results,\n );\n }\n\n /**\n * The checklists that still OWE a verdict: no review-<id>.json at all, a malformed one, or one whose\n * verdict is an un-overridden FAIL. This is the set every message lists — a checklist already PASSed or\n * OVERRIDDEN on this branch is deliberately NOT re-listed, because re-instructing it invites a redundant\n * second run and reads as though the earlier verdict did not count.\n */\n pendingChecklists(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[] {\n return required.filter((req: RequiredChecklist): boolean => {\n const status = this.resolveVerdict(req, results).status;\n // CK_WARN must be listed here beside PASS/OVERRIDDEN. A yellow verdict SHIPS — leaving it out\n // would mark the checklist owed forever, so `outstanding` never empties and the PR is refused\n // permanently no matter how many times the reviewer runs.\n return status !== CK_PASS && status !== CK_WARN && status !== CK_OVERRIDDEN;\n });\n }\n\n /**\n * The OPTIONAL checklists (`required: false`) that matched the diff but have no verdict file at all.\n *\n * This is the ONE set that separates \"nobody ran it\" from \"it failed\", and the distinction is the whole\n * feature: an optional checklist with no verdict was legitimately not run — declined by the human, or\n * skipped via `--no-optional` — so it must NOT block. An optional checklist with a RED verdict is not in\n * here (it resolves to CK_FAIL) and blocks exactly like a required one: choosing to run a reviewer and\n * then ignoring its answer would make the whole thing theater.\n *\n * A strict subset of {@link pendingChecklists}, computed here rather than at each call site so the\n * command that gates and the dashboard that reports cannot disagree about which checklists were skipped.\n */\n optionalWithoutVerdict(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[] {\n return required.filter((req: RequiredChecklist): boolean =>\n !req.required && this.resolveVerdict(req, results).status === CK_MISSING);\n }\n\n /**\n * The checklists that REFUSED: a reviewer ran, judged the change, and said no (CK_FAIL — status red with\n * no override). A strict subset of {@link pendingChecklists}, split out because it demands a completely\n * different action from the reader.\n *\n * Public so every command agrees on the set. When \"refused\" was computed ad hoc, a refusal and a\n * never-ran reviewer landed in one bucket and produced one message — \"you MUST run these N reviewer\n * subagent(s)\" — handed to an AI, which obediently re-spawned a reviewer that had already answered. It\n * refused again for the same reason, and the loop cost a full subagent run per pass while the reviewer's\n * actual finding was never shown to anyone. A refusal is a RESULT, not a missing step.\n */\n refusedChecklists(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[] {\n return required.filter((req: RequiredChecklist): boolean =>\n this.resolveVerdict(req, results).status === CK_FAIL);\n }\n\n /**\n * THE renderer for \"this reviewer refused\" — one wording, wherever the refusal surfaces. It exists as a\n * method because the text was previously inlined in {@link requiredChecklistErrors}, reachable only\n * through review.json validation, while the command layer refused earlier with its own generic message.\n * Two messages for one event is how the useful one became unreachable; there is now exactly one.\n *\n * It always quotes the reviewer's own `output` verbatim: the finding is the whole point, and an error\n * that names a checklist without saying what it objected to gives the reader nothing to fix.\n *\n * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) to that path, so the message must\n * change in two ways. It says where the record went — otherwise the move reads as data loss — and,\n * critically, it must NOT tell the reader to \"set override in review-<id>.json\", because that file no\n * longer exists. The escape hatch is therefore worded as writing a FRESH verdict file (the body can be\n * copied back out of the archive) with a human-authored override.\n */\n refusalError(req: RequiredChecklist, verdict: ChecklistVerdict, archivedPath = ''): string {\n const finding = `${verdict.detail.split('\\n').join('\\n ')}\\n`;\n const head = `Checklist \"${req.id}\" FAILED review (status:\"${VERDICT_RED}\"). The reviewer (${req.subagent}) wrote:\\n ` + finding;\n if (archivedPath === '') {\n return head +\n ` Fix it, then re-run; or set a non-empty \"override\" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`;\n }\n // Re-spawning is the LAST thing said, and only after the finding, because an instruction to spawn a\n // subagent is the one line an AI acts on first — see refusedChecklists for what that cost.\n return head +\n ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\\n` +\n ` A FRESH ${this.checklistFileName(req.id)} is now required. Fix the finding first, then have the ` +\n `\"${req.subagent}\" subagent review again and write a new verdict.\\n` +\n ` To ship anyway, a HUMAN must decide it: write a fresh ${this.checklistFileName(req.id)} (you may copy the ` +\n `body back from the archive) carrying a non-empty, human-authored \"override\" justification.`;\n }\n\n // Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.\n // A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed\n // one is skipped (a stale review-<id>.json never wedges the branch).\n //\n // It looks up the EXACT `review-<id>.json` name per required id — never a directory scan, never a prefix\n // match. That is what guarantees an archived `review-<id>.json.old` can never resolve as a live verdict:\n // the retired file sits right beside the live path, and a scan that swept the directory would hand a\n // RETIRED refusal (or worse, a retired pass) back as the current state, undoing the whole point of the\n // move in {@link archiveChecklistResult}.\n loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[] {\n const results: ChecklistResult[] = [];\n for (const req of required) {\n const p = this.checklistResultPath(reviewJsonFilePath, req.id);\n if (!fs.existsSync(p)) continue;\n const parsed = this.parseChecklistResult(p, req.id);\n if (parsed) results.push(parsed);\n }\n return results;\n }\n\n // Resolve ONE checklist's verdict from its review-<id>.json. Central so review.json enforcement AND the\n // finish-command dashboard agree on the outcome. `problem` is checked FIRST: a file whose verdict cannot\n // be read must not fall through to any shipping outcome.\n resolveVerdict(req: RequiredChecklist, results: readonly ChecklistResult[]): ChecklistVerdict {\n const result = results.find((r: ChecklistResult): boolean => r.id === req.id);\n if (!result) return new ChecklistVerdict(req.id, CK_MISSING, '');\n if (result.problem !== '') return new ChecklistVerdict(req.id, CK_BAD_FORMAT, result.problem);\n if (result.status === VERDICT_GREEN) return new ChecklistVerdict(req.id, CK_PASS, result.output);\n if (result.status === VERDICT_YELLOW) return new ChecklistVerdict(req.id, CK_WARN, result.output);\n if (result.override.trim() !== '') return new ChecklistVerdict(req.id, CK_OVERRIDDEN, result.override.trim());\n return new ChecklistVerdict(req.id, CK_FAIL, result.output);\n }\n\n /**\n * One loud complaint per checklist whose verdict file EXISTS but cannot be read as a verdict — almost\n * always one still using the removed `success` field. Public and separate from\n * {@link requiredChecklistErrors} because `wp-finish-upsert-pr` refuses on missing reviewers BEFORE it\n * parses review.json: without this, a legacy file would surface as the generic \"no verdict yet\" block\n * and the AI would re-run a reviewer that already ran instead of fixing four characters of JSON.\n */\n checklistFormatErrors(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): string[] {\n const errors: string[] = [];\n for (const req of required) {\n const verdict = this.resolveVerdict(req, results);\n if (verdict.status === CK_BAD_FORMAT) errors.push(verdict.detail);\n }\n return errors;\n }\n\n // Every matched checklist whose verdict is FAIL (reviewed, found a problem, no override) or MISSING (no\n // review-<id>.json written) → one error each, printing the reviewer's `output` verbatim.\n private requiredChecklistErrors(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): string[] {\n // Format complaints come from the ONE renderer, so wp-review-upsert-pr and wp-finish word them identically.\n const errors: string[] = this.checklistFormatErrors(required, results);\n for (const req of required) {\n const verdict = this.resolveVerdict(req, results);\n // CK_WARN ('yellow' — passed with concerns) is deliberately absent from this chain: it SHIPS.\n // The concern still reaches the PR, published in the checklist comment. Do not \"fix\" this.\n if (verdict.status === CK_FAIL) {\n // Through the ONE renderer, so this path and the command layer's refusal say the same thing.\n // No archive path here: this is validation, not the act of retiring the verdict.\n errors.push(this.refusalError(req, verdict));\n } else if (verdict.status === CK_MISSING) {\n // An OPTIONAL checklist with no verdict was legitimately not run — the human was offered it\n // and declined (or `--no-optional` skipped the offer). Demanding it here would make\n // `required: false` mean nothing. Note the CK_FAIL branch above deliberately has no such\n // exemption: once an optional reviewer RUNS, its refusal counts.\n if (!req.required) continue;\n const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';\n errors.push(\n `Checklist \"${req.id}\" MATCHED this diff but has no verdict. Spawn the \"${req.subagent}\" subagent to review it, ` +\n `then write ${this.checklistFileName(req.id)} with ` +\n `{\"id\":\"${req.id}\",\"status\":\"${VERDICT_GREEN}\",\"output\":\"…\",\"override\":\"\"}.${doc}`,\n );\n }\n }\n return errors;\n }\n\n private checklistFileName(checklistId: string): string {\n return `review-${checklistId}.json`;\n }\n\n /**\n * THE renderer for a reviewer's verdict schema — with the reviewer's own `id` already filled in and,\n * when known, the exact file it must write.\n *\n * There is one because a verdict schema that lives anywhere a human maintains it goes stale, and a\n * reviewer follows the stale copy. That is not a hypothetical: when `success` was replaced by the\n * tri-state `status`, hand-written `.claude/agents/*.md` files kept documenting `success`, and a real\n * PR had to carry \"the verdict format in your own agent .md file is OUT OF DATE\" in the spawn prompt to\n * work around it. Every printed copy — the stage-② roster, the generated per-reviewer instructions\n * file, and the complaint raised against a malformed verdict — now comes from here.\n *\n * `verdictPath` may be '' when the caller is describing the shape rather than a specific file.\n */\n verdictSchemaFor(id: string, verdictPath = '', indent = ' '): string {\n const lines = [\n `${indent}{ \"id\": \"${id}\", \"status\": \"${VERDICT_GREEN} | ${VERDICT_YELLOW} | ${VERDICT_RED}\", ` +\n `\"output\": \"what you checked / found\", \"override\": \"\" }`,\n `${indent} ${VERDICT_GREEN} → passes, nothing to flag`,\n `${indent} ${VERDICT_YELLOW} → passes WITH CONCERNS; nothing is blocked and the concern is published on the PR`,\n `${indent} ${VERDICT_RED} → REFUSES the PR (set a non-empty \"override\" to ship anyway with a stated justification)`,\n `${indent}Prefer \"${VERDICT_YELLOW}\" over red-plus-override when the change is acceptable but worth a human's`,\n `${indent}attention — an override reads as a deliberately-accepted defect, a yellow reads as a note.`,\n ];\n if (verdictPath !== '') lines.push(`${indent}File: ${verdictPath}`);\n return lines.join('\\n');\n }\n\n /**\n * Parse one review-<id>.json into a ChecklistResult. `null` ONLY when the bytes do not parse as a JSON\n * object at all — that tolerance is why a half-written file never wedges a branch, and it degrades to\n * the same \"no verdict yet\" message as an absent file, which is honest (nothing readable is there).\n *\n * A file that DOES parse always yields a result, even when its verdict is unreadable, carrying the\n * complaint in `problem`. Returning `null` for those instead would collapse \"wrote a verdict in the old\n * format\" into \"never wrote a verdict\" and send the AI off to re-run a reviewer that already ran.\n */\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field\n private parseChecklistResult(filePath: string, id: string): ChecklistResult | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unparseable per-checklist file is skipped, not fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed below\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return null;\n const output = typeof raw['output'] === 'string' ? (raw['output'] as string) : '';\n const override = typeof raw['override'] === 'string' ? (raw['override'] as string) : '';\n const status = typeof raw['status'] === 'string' ? (raw['status'] as string).trim().toLowerCase() : '';\n return new ChecklistResult(id, status, output, override, this.statusProblem(filePath, id, status, raw));\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * '' when `status` is one of the three colors. Otherwise the complaint to show the AI verbatim. The\n * legacy-`success` case gets its OWN message: `success` was removed outright (no compatibility mode),\n * and a reviewer told only \"status must be green|yellow|red\" cannot tell whether it wrote the wrong\n * value or is using a field that no longer exists.\n */\n // webpieces-disable no-any-unknown -- opaque parsed JSON; only tested for key presence here\n private statusProblem(filePath: string, id: string, status: string, raw: Record<string, unknown>): string {\n // webpieces-disable no-any-unknown -- comparing against the readonly literal tuple of valid colors\n if ((VERDICT_STATUSES as readonly string[]).includes(status)) return '';\n // The ONE renderer — see verdictSchemaFor. A second copy here is what let the old `success` shape\n // survive in print after it was removed from the parser.\n const shape = this.verdictSchemaFor(id, filePath);\n if ('success' in raw) {\n return `Checklist \"${id}\" wrote its verdict with the REMOVED \"success\" field. It is now a tri-state ` +\n `\"status\" — there is no compatibility mode. Rewrite the file as:\\n${shape}`;\n }\n return `Checklist \"${id}\" wrote a verdict with no valid \"status\" (got ${JSON.stringify(status)}). ` +\n `It must be exactly one of ${VERDICT_STATUSES.join(', ')}:\\n${shape}`;\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath, required);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { PR_REVIEW_DIR } from './constants';\nimport { DotWebpieces, dotWebpieces } from './state-dir';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\nimport {\n VERDICT_GREEN,\n VERDICT_YELLOW,\n VERDICT_RED,\n VERDICT_STATUSES,\n ChecklistResult,\n RequiredChecklist,\n ChecklistReviewContext,\n ReviewJson,\n CK_PASS,\n CK_WARN,\n CK_OVERRIDDEN,\n CK_FAIL,\n CK_MISSING,\n CK_BAD_FORMAT,\n ChecklistVerdict,\n PrContext,\n} from './review-json-data';\n\n// Re-exported so review-json.ts stays the single import site for the whole review vocabulary: the data\n// classes moved out to keep this file under the file-size limit, NOT to give callers a second module to\n// learn. Every existing `from './review-json'` import keeps resolving.\nexport {\n VERDICT_GREEN,\n VERDICT_YELLOW,\n VERDICT_RED,\n VERDICT_STATUSES,\n ChecklistResult,\n RequiredChecklist,\n ChecklistReviewContext,\n ReviewJson,\n CK_PASS,\n CK_WARN,\n CK_OVERRIDDEN,\n CK_FAIL,\n CK_MISSING,\n CK_BAD_FORMAT,\n ChecklistVerdict,\n PrContext,\n};\n\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n// Where `wp-finish-upsert-pr` retires the review.json it just consumed, and the note it stamps on the way.\n// The key sorts first in the written JSON because it is written first — an AI that opens the file to see\n// whether it can reuse the review reads what the file IS before it reads a title it might be tempted to keep.\nconst OLD_REVIEW_FILE = 'old-review.json';\nconst ARCHIVE_NOTE_KEY = '_ARCHIVED_AUDIT_ONLY';\nconst ARCHIVE_NOTE =\n 'ARCHIVE — this is the review from the PREVIOUS wp-finish-upsert-pr run on this branch, kept for audit ' +\n 'purposes only. It is NOT the review for a new review round: it describes the code as of the last PR ' +\n 'update, which has since moved. If you are reviewing again, write a FRESH review.json at the path ' +\n 'pnpm wp-review-upsert-pr prints; do not copy this file\\'s title, summary or risk level forward without ' +\n 're-deciding each one. Overwritten by every finish, so only the most recent review is ever here.';\n\n// The same stamp, for a retired per-checklist verdict. Verdict files get their OWN wording because the two\n// archives answer different questions: old-review.json holds a description of the code, this holds a\n// REVIEWER'S DECISION. The one thing that must not happen is a reader treating an archived red as the live\n// verdict — the whole reason the file was moved rather than copied — so the note says that outright.\nconst CHECKLIST_ARCHIVE_NOTE =\n 'ARCHIVE — this is a checklist verdict from a PREVIOUS reviewer run on this branch, kept for audit ' +\n 'purposes only. It is NOT a live verdict and must never be read back as one: it was RETIRED because it ' +\n 'refused the PR, and the gate moved it here so the only way forward is a FRESH review-<id>.json written ' +\n 'by a real reviewer run. Do not copy its status back onto the live path to get past the gate. ' +\n 'Overwritten by every retirement, so only the most recently retired verdict is ever here.';\n\n/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewJsonService {\n constructor(private readonly dotDir: DotWebpieces = dotWebpieces) {}\n\n // The per-feature PR working dir: `<worktree>/.webpieces/pr-review/<feature>`. AI-WRITABLE scope,\n // not local() — an agent AUTHORS review.json here, and each reviewer subagent authors its own\n // review-<id>.json beside it. A worktree-isolated agent's Write is refused for any path under the\n // shared checkout, which is where local() puts this, so local() made both files unwritable by the\n // very agents the flow instructs to write them. See DotWebpieces.aiWritable() for the full account.\n prDirFor(repoRoot: string, featureName: string): string {\n return this.dotDir.aiWritableFile(repoRoot, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Absolute path of the pr-context.json for a feature (the diff base/head + changed files).\n prContextPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'pr-context.json');\n }\n\n // Where a consumed review.json is archived to, beside it. Always the SAME path — it holds the last\n // review and only the last one, so it can never be mistaken for a series that means something.\n oldReviewJsonPath(reviewJsonFilePath: string): string {\n return path.join(path.dirname(reviewJsonFilePath), OLD_REVIEW_FILE);\n }\n\n /**\n * Retire the review `wp-finish-upsert-pr` just used: move review.json to old-review.json, stamped with a\n * note saying what it is. Returns the archive path, or '' when there was nothing to archive.\n *\n * The point is the MOVE, not the copy. review.json left in place after a PR is posted is a live-looking\n * file describing a review that already happened, and the next run of stage ② on this branch finds it\n * sitting there — so a reviewer subagent that judges the PR's stated intent (its title, summary or risk\n * level) can read the previous run's review and return GREEN against a title that no longer exists.\n * Nothing in the verdict distinguishes that from a real pass. Moving it means the only way to reach\n * finish again is to write a fresh one, and {@link loadReviewJson} points at the archive when it is\n * missing so the archive reads as an audit trail rather than as a lost file.\n *\n * Called only after the PR is actually up: a finish that failed before publishing must stay re-runnable.\n */\n archiveReviewJson(reviewJsonFilePath: string): string {\n if (!fs.existsSync(reviewJsonFilePath)) return '';\n const archivePath = this.oldReviewJsonPath(reviewJsonFilePath);\n const raw = fs.readFileSync(reviewJsonFilePath, 'utf8');\n fs.writeFileSync(archivePath, this.archivedBody(raw, ARCHIVE_NOTE));\n fs.rmSync(reviewJsonFilePath);\n return archivePath;\n }\n\n /**\n * The archived bytes: the original JSON with an AUDIT-ONLY note as its FIRST key, so anything that opens\n * the file — human or AI — reads what it is before it reads any of its content.\n *\n * `note` is a parameter rather than a constant because two different files are archived here (review.json\n * and review-<id>.json) and they need to say different things, while the stamping MECHANICS — parse,\n * note first, original keys in order, fall back to raw — are identical. One implementation, two texts;\n * a second copy of this method would be the thing that drifts.\n *\n * Falls back to the raw bytes when they do not parse. For review.json `loadReviewJson` has already\n * accepted the file so that is close to impossible, but a verdict file is written by a subagent and may\n * be half-written or not an object at all — and preserving the original always beats losing it to a\n * stamping failure, since the archive exists precisely to be the record.\n */\n private archivedBody(raw: string, note: string): string {\n const parsed = this.tryParseObject(raw);\n if (parsed === null) return raw;\n // webpieces-disable no-any-unknown -- re-serializing opaque review fields verbatim; only the key ORDER is ours\n const stamped: Record<string, unknown> = {};\n stamped[ARCHIVE_NOTE_KEY] = note;\n for (const key of Object.keys(parsed)) stamped[key] = parsed[key];\n return JSON.stringify(stamped, null, 2) + '\\n';\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON; the caller only re-serializes it\n private tryParseObject(raw: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unparseable bytes are archived verbatim, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque; only its key order is used\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null;\n return parsed;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * Persist the PR's diff context so reviewer subagents can read the changed-file set + the exact base\n * sha. Returns the file path written.\n *\n * ALSO writes an immutable per-stage snapshot under `stages/<stage>.json` when `stage` is given.\n * `pr-context.json` is overwritten by each stage, so by the time anything goes wrong the earlier\n * states are gone — and \"what did the tooling think the diff was at stage ①, vs ②, vs ③?\" is exactly\n * the question you need answered when debugging a review that went sideways. The snapshots make the\n * review system auditable by an AI reviewing IT, which the single mutable file never could.\n */\n writePrContext(repoRoot: string, featureName: string, context: PrContext, stage = ''): string {\n const dir = this.prDirFor(repoRoot, featureName);\n fs.mkdirSync(dir, { recursive: true });\n const body = JSON.stringify(context, null, 2) + '\\n';\n const p = this.prContextPath(repoRoot, featureName);\n fs.writeFileSync(p, body);\n if (stage !== '') {\n const stagesDir = path.join(dir, 'stages');\n fs.mkdirSync(stagesDir, { recursive: true });\n fs.writeFileSync(path.join(stagesDir, `${stage}.json`), body);\n }\n return p;\n }\n\n /**\n * The review context for a feature, recovered from the pr-context.json wp-start-upsert-pr already wrote.\n * Lets wp-finish-upsert-pr's \"you still owe me review-<id>.json\" message inline the SAME self-sufficient\n * per-reviewer block start printed, instead of a checklist name and an indirection. Empty when the file\n * is absent or unreadable — the block then just omits those lines.\n */\n reviewContextFor(repoRoot: string, featureName: string): ChecklistReviewContext {\n const p = this.prContextPath(repoRoot, featureName);\n if (!fs.existsSync(p)) return new ChecklistReviewContext();\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context file degrades to fewer printed lines, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed on the next line\n const raw = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;\n const base = typeof raw['base'] === 'string' ? (raw['base'] as string) : '';\n // Recover the REPRODUCE command rather than re-deriving it: a context written by an older\n // pr-gate has no diffCommand, and guessing `<base> HEAD` there would resurrect the exact\n // empty-on-a-dirty-tree bug this field exists to kill. Absent ⇒ omit the line entirely.\n const cmd = typeof raw['diffCommand'] === 'string' ? (raw['diffCommand'] as string) : '';\n const diffDir = typeof raw['diffDir'] === 'string' ? (raw['diffDir'] as string) : '';\n const dirty = raw['dirty'] === true;\n return new ChecklistReviewContext(base, p, cmd === '' ? '' : `${cmd} -- <file>`, diffDir, dirty);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return new ChecklistReviewContext('', p);\n }\n }\n\n // Copy-paste schema both commands print. `required` is the set of checklists the diff MATCHED; empty\n // ⇒ output identical to a repo with no checklists. Non-empty ⇒ appends per-checklist instructions\n // naming the reviewer subagent + doc + the review-<id>.json to write.\n reviewJsonSchemaHint(filePath: string): string {\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]\\n` +\n `}`\n );\n }\n\n /**\n * The extra line the \"no review.json\" complaint carries when a PREVIOUS review was archived here. It\n * turns a bare \"not found\" — which reads as data loss, and invites hunting for the file — into the fact:\n * the last finish consumed it, and the archive is audit material, not a review to reuse.\n */\n private archivedReviewHint(filePath: string): string {\n const archive = this.oldReviewJsonPath(filePath);\n if (!fs.existsSync(archive)) return '';\n return `\\nA PREVIOUS review was archived to ${archive} when the last pnpm wp-finish-upsert-pr consumed it.\\n` +\n `That file is for AUDIT ONLY — it reviews code this branch has since moved past. Write a fresh one:`;\n }\n\n // The per-checklist review file path that sits beside review.json: review-<id>.json.\n checklistResultPath(reviewJsonFilePath: string, checklistId: string): string {\n return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);\n }\n\n /**\n * Where a RETIRED verdict for one checklist goes: `review-<id>.json.old`, beside the live path.\n *\n * Mirrors {@link oldReviewJsonPath} deliberately, including its single-slot rule: ALWAYS the same path,\n * so it holds the last retired verdict and only the last one. A series (`.old.old`, `.old.1`) would read\n * as though the number of retirements meant something, and nothing downstream can interpret that — the\n * one fact worth keeping is \"this checklist refused before, here is what it said\".\n */\n oldChecklistResultPath(reviewJsonFilePath: string, checklistId: string): string {\n return `${this.checklistResultPath(reviewJsonFilePath, checklistId)}.old`;\n }\n\n /**\n * Retire one checklist's verdict: MOVE review-<id>.json to review-<id>.json.old, stamped with a note\n * saying what it is. Returns the archive path, or '' when there was nothing to archive.\n *\n * The point is the MOVE, exactly as in {@link archiveReviewJson}. A red verdict left on the live path is\n * re-read by the next run and re-reported as the CURRENT state of the branch, so the branch keeps being\n * refused for a finding that may already be fixed — and the fix, when it comes, silently overwrites the\n * only record that the gate ever refused anything. Moving it makes the refusal durable and makes a fresh\n * reviewer run the only way forward, which is the honest requirement: the old verdict judged code that\n * has since changed.\n *\n * Safe by construction for RED verdicts specifically, which is why the caller only archives on CK_FAIL:\n * a red verdict is never reusable — it always blocks — so nothing is lost by moving it. Green and yellow\n * verdicts ARE deliberately reused across finish attempts, and retiring one would force a needless (and\n * expensive) subagent re-run.\n */\n archiveChecklistResult(reviewJsonFilePath: string, checklistId: string): string {\n const livePath = this.checklistResultPath(reviewJsonFilePath, checklistId);\n if (!fs.existsSync(livePath)) return '';\n const archivePath = this.oldChecklistResultPath(reviewJsonFilePath, checklistId);\n const raw = fs.readFileSync(livePath, 'utf8');\n fs.writeFileSync(archivePath, this.archivedBody(raw, CHECKLIST_ARCHIVE_NOTE));\n fs.rmSync(livePath);\n return archivePath;\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one\n * must have a well-formed, passing (or overridden) review-<id>.json or a validation error is raised\n * alongside the usual ones so the AI gets ONE message.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.${this.archivedReviewHint(filePath)}\\n\\n` +\n `${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n const results = this.loadChecklistResults(filePath, required);\n for (const err of this.requiredChecklistErrors(required, results)) errors.push(err);\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n results,\n );\n }\n\n /**\n * The checklists that still OWE a verdict: no review-<id>.json at all, a malformed one, or one whose\n * verdict is an un-overridden FAIL. This is the set every message lists — a checklist already PASSed or\n * OVERRIDDEN on this branch is deliberately NOT re-listed, because re-instructing it invites a redundant\n * second run and reads as though the earlier verdict did not count.\n */\n pendingChecklists(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[] {\n return required.filter((req: RequiredChecklist): boolean => {\n const status = this.resolveVerdict(req, results).status;\n // CK_WARN must be listed here beside PASS/OVERRIDDEN. A yellow verdict SHIPS — leaving it out\n // would mark the checklist owed forever, so `outstanding` never empties and the PR is refused\n // permanently no matter how many times the reviewer runs.\n return status !== CK_PASS && status !== CK_WARN && status !== CK_OVERRIDDEN;\n });\n }\n\n /**\n * The OPTIONAL checklists (`required: false`) that matched the diff but have no verdict file at all.\n *\n * This is the ONE set that separates \"nobody ran it\" from \"it failed\", and the distinction is the whole\n * feature: an optional checklist with no verdict was legitimately not run — declined by the human, or\n * skipped via `--no-optional` — so it must NOT block. An optional checklist with a RED verdict is not in\n * here (it resolves to CK_FAIL) and blocks exactly like a required one: choosing to run a reviewer and\n * then ignoring its answer would make the whole thing theater.\n *\n * A strict subset of {@link pendingChecklists}, computed here rather than at each call site so the\n * command that gates and the dashboard that reports cannot disagree about which checklists were skipped.\n */\n optionalWithoutVerdict(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[] {\n return required.filter((req: RequiredChecklist): boolean =>\n !req.required && this.resolveVerdict(req, results).status === CK_MISSING);\n }\n\n /**\n * The checklists that REFUSED: a reviewer ran, judged the change, and said no (CK_FAIL — status red with\n * no override). A strict subset of {@link pendingChecklists}, split out because it demands a completely\n * different action from the reader.\n *\n * Public so every command agrees on the set. When \"refused\" was computed ad hoc, a refusal and a\n * never-ran reviewer landed in one bucket and produced one message — \"you MUST run these N reviewer\n * subagent(s)\" — handed to an AI, which obediently re-spawned a reviewer that had already answered. It\n * refused again for the same reason, and the loop cost a full subagent run per pass while the reviewer's\n * actual finding was never shown to anyone. A refusal is a RESULT, not a missing step.\n */\n refusedChecklists(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[] {\n return required.filter((req: RequiredChecklist): boolean =>\n this.resolveVerdict(req, results).status === CK_FAIL);\n }\n\n /**\n * THE renderer for \"this reviewer refused\" — one wording, wherever the refusal surfaces. It exists as a\n * method because the text was previously inlined in {@link requiredChecklistErrors}, reachable only\n * through review.json validation, while the command layer refused earlier with its own generic message.\n * Two messages for one event is how the useful one became unreachable; there is now exactly one.\n *\n * It always quotes the reviewer's own `output` verbatim: the finding is the whole point, and an error\n * that names a checklist without saying what it objected to gives the reader nothing to fix.\n *\n * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) to that path, so the message must\n * change in two ways. It says where the record went — otherwise the move reads as data loss — and,\n * critically, it must NOT tell the reader to \"set override in review-<id>.json\", because that file no\n * longer exists. The escape hatch is therefore worded as writing a FRESH verdict file (the body can be\n * copied back out of the archive) with a human-authored override.\n */\n refusalError(req: RequiredChecklist, verdict: ChecklistVerdict, archivedPath = ''): string {\n const finding = `${verdict.detail.split('\\n').join('\\n ')}\\n`;\n const head = `Checklist \"${req.id}\" FAILED review (status:\"${VERDICT_RED}\"). The reviewer (${req.subagent}) wrote:\\n ` + finding;\n if (archivedPath === '') {\n return head +\n ` Fix it, then re-run; or set a non-empty \"override\" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`;\n }\n // Re-spawning is the LAST thing said, and only after the finding, because an instruction to spawn a\n // subagent is the one line an AI acts on first — see refusedChecklists for what that cost.\n return head +\n ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\\n` +\n ` A FRESH ${this.checklistFileName(req.id)} is now required. Fix the finding first, then have the ` +\n `\"${req.subagent}\" subagent review again and write a new verdict.\\n` +\n ` To ship anyway, a HUMAN must decide it: write a fresh ${this.checklistFileName(req.id)} (you may copy the ` +\n `body back from the archive) carrying a non-empty, human-authored \"override\" justification.`;\n }\n\n // Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.\n // A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed\n // one is skipped (a stale review-<id>.json never wedges the branch).\n //\n // It looks up the EXACT `review-<id>.json` name per required id — never a directory scan, never a prefix\n // match. That is what guarantees an archived `review-<id>.json.old` can never resolve as a live verdict:\n // the retired file sits right beside the live path, and a scan that swept the directory would hand a\n // RETIRED refusal (or worse, a retired pass) back as the current state, undoing the whole point of the\n // move in {@link archiveChecklistResult}.\n loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[] {\n const results: ChecklistResult[] = [];\n for (const req of required) {\n const p = this.checklistResultPath(reviewJsonFilePath, req.id);\n if (!fs.existsSync(p)) continue;\n const parsed = this.parseChecklistResult(p, req.id);\n if (parsed) results.push(parsed);\n }\n return results;\n }\n\n // Resolve ONE checklist's verdict from its review-<id>.json. Central so review.json enforcement AND the\n // finish-command dashboard agree on the outcome. `problem` is checked FIRST: a file whose verdict cannot\n // be read must not fall through to any shipping outcome.\n resolveVerdict(req: RequiredChecklist, results: readonly ChecklistResult[]): ChecklistVerdict {\n const result = results.find((r: ChecklistResult): boolean => r.id === req.id);\n if (!result) return new ChecklistVerdict(req.id, CK_MISSING, '');\n if (result.problem !== '') return new ChecklistVerdict(req.id, CK_BAD_FORMAT, result.problem);\n if (result.status === VERDICT_GREEN) return new ChecklistVerdict(req.id, CK_PASS, result.output);\n if (result.status === VERDICT_YELLOW) return new ChecklistVerdict(req.id, CK_WARN, result.output);\n if (result.override.trim() !== '') return new ChecklistVerdict(req.id, CK_OVERRIDDEN, result.override.trim());\n return new ChecklistVerdict(req.id, CK_FAIL, result.output);\n }\n\n /**\n * One loud complaint per checklist whose verdict file EXISTS but cannot be read as a verdict — almost\n * always one still using the removed `success` field. Public and separate from\n * {@link requiredChecklistErrors} because `wp-finish-upsert-pr` refuses on missing reviewers BEFORE it\n * parses review.json: without this, a legacy file would surface as the generic \"no verdict yet\" block\n * and the AI would re-run a reviewer that already ran instead of fixing four characters of JSON.\n */\n checklistFormatErrors(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): string[] {\n const errors: string[] = [];\n for (const req of required) {\n const verdict = this.resolveVerdict(req, results);\n if (verdict.status === CK_BAD_FORMAT) errors.push(verdict.detail);\n }\n return errors;\n }\n\n // Every matched checklist whose verdict is FAIL (reviewed, found a problem, no override) or MISSING (no\n // review-<id>.json written) → one error each, printing the reviewer's `output` verbatim.\n private requiredChecklistErrors(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): string[] {\n // Format complaints come from the ONE renderer, so wp-review-upsert-pr and wp-finish word them identically.\n const errors: string[] = this.checklistFormatErrors(required, results);\n for (const req of required) {\n const verdict = this.resolveVerdict(req, results);\n // CK_WARN ('yellow' — passed with concerns) is deliberately absent from this chain: it SHIPS.\n // The concern still reaches the PR, published in the checklist comment. Do not \"fix\" this.\n if (verdict.status === CK_FAIL) {\n // Through the ONE renderer, so this path and the command layer's refusal say the same thing.\n // No archive path here: this is validation, not the act of retiring the verdict.\n errors.push(this.refusalError(req, verdict));\n } else if (verdict.status === CK_MISSING) {\n // An OPTIONAL checklist with no verdict was legitimately not run — the human was offered it\n // and declined (or `--no-optional` skipped the offer). Demanding it here would make\n // `required: false` mean nothing. Note the CK_FAIL branch above deliberately has no such\n // exemption: once an optional reviewer RUNS, its refusal counts.\n if (!req.required) continue;\n const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';\n errors.push(\n `Checklist \"${req.id}\" MATCHED this diff but has no verdict. Spawn the \"${req.subagent}\" subagent to review it, ` +\n `then write ${this.checklistFileName(req.id)} with ` +\n `{\"id\":\"${req.id}\",\"status\":\"${VERDICT_GREEN}\",\"output\":\"…\",\"override\":\"\"}.${doc}`,\n );\n }\n }\n return errors;\n }\n\n private checklistFileName(checklistId: string): string {\n return `review-${checklistId}.json`;\n }\n\n /**\n * THE renderer for a reviewer's verdict schema — with the reviewer's own `id` already filled in and,\n * when known, the exact file it must write.\n *\n * There is one because a verdict schema that lives anywhere a human maintains it goes stale, and a\n * reviewer follows the stale copy. That is not a hypothetical: when `success` was replaced by the\n * tri-state `status`, hand-written `.claude/agents/*.md` files kept documenting `success`, and a real\n * PR had to carry \"the verdict format in your own agent .md file is OUT OF DATE\" in the spawn prompt to\n * work around it. Every printed copy — the stage-② roster, the generated per-reviewer instructions\n * file, and the complaint raised against a malformed verdict — now comes from here.\n *\n * `verdictPath` may be '' when the caller is describing the shape rather than a specific file.\n */\n verdictSchemaFor(id: string, verdictPath = '', indent = ' '): string {\n const lines = [\n `${indent}{ \"id\": \"${id}\", \"status\": \"${VERDICT_GREEN} | ${VERDICT_YELLOW} | ${VERDICT_RED}\", ` +\n `\"output\": \"what you checked / found\", \"override\": \"\" }`,\n `${indent} ${VERDICT_GREEN} → passes, nothing to flag`,\n `${indent} ${VERDICT_YELLOW} → passes WITH CONCERNS; nothing is blocked and the concern is published on the PR`,\n `${indent} ${VERDICT_RED} → REFUSES the PR (set a non-empty \"override\" to ship anyway with a stated justification)`,\n `${indent}Prefer \"${VERDICT_YELLOW}\" over red-plus-override when the change is acceptable but worth a human's`,\n `${indent}attention — an override reads as a deliberately-accepted defect, a yellow reads as a note.`,\n ];\n if (verdictPath !== '') lines.push(`${indent}File: ${verdictPath}`);\n return lines.join('\\n');\n }\n\n /**\n * Parse one review-<id>.json into a ChecklistResult. `null` ONLY when the bytes do not parse as a JSON\n * object at all — that tolerance is why a half-written file never wedges a branch, and it degrades to\n * the same \"no verdict yet\" message as an absent file, which is honest (nothing readable is there).\n *\n * A file that DOES parse always yields a result, even when its verdict is unreadable, carrying the\n * complaint in `problem`. Returning `null` for those instead would collapse \"wrote a verdict in the old\n * format\" into \"never wrote a verdict\" and send the AI off to re-run a reviewer that already ran.\n */\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field\n private parseChecklistResult(filePath: string, id: string): ChecklistResult | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unparseable per-checklist file is skipped, not fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed below\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return null;\n const output = typeof raw['output'] === 'string' ? (raw['output'] as string) : '';\n const override = typeof raw['override'] === 'string' ? (raw['override'] as string) : '';\n const status = typeof raw['status'] === 'string' ? (raw['status'] as string).trim().toLowerCase() : '';\n return new ChecklistResult(id, status, output, override, this.statusProblem(filePath, id, status, raw));\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * '' when `status` is one of the three colors. Otherwise the complaint to show the AI verbatim. The\n * legacy-`success` case gets its OWN message: `success` was removed outright (no compatibility mode),\n * and a reviewer told only \"status must be green|yellow|red\" cannot tell whether it wrote the wrong\n * value or is using a field that no longer exists.\n */\n // webpieces-disable no-any-unknown -- opaque parsed JSON; only tested for key presence here\n private statusProblem(filePath: string, id: string, status: string, raw: Record<string, unknown>): string {\n // webpieces-disable no-any-unknown -- comparing against the readonly literal tuple of valid colors\n if ((VERDICT_STATUSES as readonly string[]).includes(status)) return '';\n // The ONE renderer — see verdictSchemaFor. A second copy here is what let the old `success` shape\n // survive in print after it was removed from the parser.\n const shape = this.verdictSchemaFor(id, filePath);\n if ('success' in raw) {\n return `Checklist \"${id}\" wrote its verdict with the REMOVED \"success\" field. It is now a tri-state ` +\n `\"status\" — there is no compatibility mode. Rewrite the file as:\\n${shape}`;\n }\n return `Checklist \"${id}\" wrote a verdict with no valid \"status\" (got ${JSON.stringify(status)}). ` +\n `It must be exactly one of ${VERDICT_STATUSES.join(', ')}:\\n${shape}`;\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath, required);\n}\n"]}
|
|
@@ -20,10 +20,14 @@ export declare class StateMigrationReport {
|
|
|
20
20
|
* with three failure modes, a Windows hazard, and `rename(2)` silently REPLACING the link). After a
|
|
21
21
|
* migration the legacy `<worktree>/.webpieces/` is simply gone; nothing takes its place.
|
|
22
22
|
*
|
|
23
|
-
* WHY it must exist: those directories hold REAL in-flight state — a half-finished 3-point
|
|
24
|
-
* `merge-info/staged/<branch
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* WHY it must exist: those directories hold REAL in-flight state — above all a half-finished 3-point
|
|
24
|
+
* merge under `merge-info/staged/<branch>/`. The first invocation under the new scheme must not orphan
|
|
25
|
+
* them; a merge an agent is standing in the middle of is not recoverable by re-running anything.
|
|
26
|
+
*
|
|
27
|
+
* WHAT IT MUST NOT TOUCH: the `keepInPlace` leaves — `pr-review/` today. Those are not legacy at all;
|
|
28
|
+
* `DotWebpieces.aiWritable()` resolves them to the worktree root ON PURPOSE, because that is the only
|
|
29
|
+
* directory a worktree-isolated coding agent is permitted to write into. Draining one would delete the
|
|
30
|
+
* live review directory out from under the agent writing it.
|
|
27
31
|
*
|
|
28
32
|
* WHY a plain move suffices: the destination namespace is created FOR this worktree, so it is empty in
|
|
29
33
|
* the ordinary case and the whole tree moves in one `rename`. Where something is already there (a
|
|
@@ -46,8 +50,14 @@ export declare class StateDirMigrator {
|
|
|
46
50
|
/**
|
|
47
51
|
* Drain `legacyDir` into `targetDir`. A no-op when they are the same directory or when the legacy
|
|
48
52
|
* dir does not exist / is not a real directory (a symlink means migration already happened).
|
|
53
|
+
*
|
|
54
|
+
* `keepInPlace` names the TOP-LEVEL leaves of `legacyDir` that are NOT legacy — the ones
|
|
55
|
+
* `DotWebpieces.aiWritable()` deliberately resolves to the worktree's own root because a
|
|
56
|
+
* worktree-isolated coding agent cannot write anywhere else (today: `pr-review/`). Sweeping those
|
|
57
|
+
* into the namespace would move a live directory out from under the agent that is mid-way through
|
|
58
|
+
* writing it, so they are skipped and NOT reported as kept — nothing about them is unresolved.
|
|
49
59
|
*/
|
|
50
|
-
migrate(legacyDir: string, targetDir: string): StateMigrationReport;
|
|
60
|
+
migrate(legacyDir: string, targetDir: string, keepInPlace: readonly string[]): StateMigrationReport;
|
|
51
61
|
/**
|
|
52
62
|
* Move everything under `<legacyRoot>/<relative>` to `<targetRoot>/<relative>`.
|
|
53
63
|
*
|
|
@@ -55,6 +65,10 @@ export declare class StateDirMigrator {
|
|
|
55
65
|
* in-flight `merge-info/staged/<branch>/` intact rather than copying it file by file and risking a
|
|
56
66
|
* half-moved merge. Only when the destination is an existing DIRECTORY do we descend and consider
|
|
57
67
|
* its children individually.
|
|
68
|
+
*
|
|
69
|
+
* `keepInPlace` is consulted at the TOP LEVEL only (`relative === ''`), because that is the scope
|
|
70
|
+
* `DotWebpieces.aiWritable()` assigns: a whole leaf of `.webpieces/` either lives in the worktree or
|
|
71
|
+
* it does not. A nested `pr-review/` under some other home is ordinary legacy state.
|
|
58
72
|
*/
|
|
59
73
|
private drain;
|
|
60
74
|
private relocate;
|
|
@@ -31,10 +31,14 @@ exports.StateMigrationReport = StateMigrationReport;
|
|
|
31
31
|
* with three failure modes, a Windows hazard, and `rename(2)` silently REPLACING the link). After a
|
|
32
32
|
* migration the legacy `<worktree>/.webpieces/` is simply gone; nothing takes its place.
|
|
33
33
|
*
|
|
34
|
-
* WHY it must exist: those directories hold REAL in-flight state — a half-finished 3-point
|
|
35
|
-
* `merge-info/staged/<branch
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* WHY it must exist: those directories hold REAL in-flight state — above all a half-finished 3-point
|
|
35
|
+
* merge under `merge-info/staged/<branch>/`. The first invocation under the new scheme must not orphan
|
|
36
|
+
* them; a merge an agent is standing in the middle of is not recoverable by re-running anything.
|
|
37
|
+
*
|
|
38
|
+
* WHAT IT MUST NOT TOUCH: the `keepInPlace` leaves — `pr-review/` today. Those are not legacy at all;
|
|
39
|
+
* `DotWebpieces.aiWritable()` resolves them to the worktree root ON PURPOSE, because that is the only
|
|
40
|
+
* directory a worktree-isolated coding agent is permitted to write into. Draining one would delete the
|
|
41
|
+
* live review directory out from under the agent writing it.
|
|
38
42
|
*
|
|
39
43
|
* WHY a plain move suffices: the destination namespace is created FOR this worktree, so it is empty in
|
|
40
44
|
* the ordinary case and the whole tree moves in one `rename`. Where something is already there (a
|
|
@@ -57,8 +61,14 @@ let StateDirMigrator = class StateDirMigrator {
|
|
|
57
61
|
/**
|
|
58
62
|
* Drain `legacyDir` into `targetDir`. A no-op when they are the same directory or when the legacy
|
|
59
63
|
* dir does not exist / is not a real directory (a symlink means migration already happened).
|
|
64
|
+
*
|
|
65
|
+
* `keepInPlace` names the TOP-LEVEL leaves of `legacyDir` that are NOT legacy — the ones
|
|
66
|
+
* `DotWebpieces.aiWritable()` deliberately resolves to the worktree's own root because a
|
|
67
|
+
* worktree-isolated coding agent cannot write anywhere else (today: `pr-review/`). Sweeping those
|
|
68
|
+
* into the namespace would move a live directory out from under the agent that is mid-way through
|
|
69
|
+
* writing it, so they are skipped and NOT reported as kept — nothing about them is unresolved.
|
|
60
70
|
*/
|
|
61
|
-
migrate(legacyDir, targetDir) {
|
|
71
|
+
migrate(legacyDir, targetDir, keepInPlace) {
|
|
62
72
|
const report = new StateMigrationReport();
|
|
63
73
|
if (path.resolve(legacyDir) === path.resolve(targetDir))
|
|
64
74
|
return report;
|
|
@@ -67,7 +77,7 @@ let StateDirMigrator = class StateDirMigrator {
|
|
|
67
77
|
if (!this.isRealDirectory(legacyDir))
|
|
68
78
|
return report;
|
|
69
79
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
70
|
-
this.drain(legacyDir, targetDir, '', report);
|
|
80
|
+
this.drain(legacyDir, targetDir, '', report, new Set(keepInPlace));
|
|
71
81
|
this.removeIfEmpty(legacyDir);
|
|
72
82
|
}
|
|
73
83
|
catch (err) {
|
|
@@ -85,10 +95,16 @@ let StateDirMigrator = class StateDirMigrator {
|
|
|
85
95
|
* in-flight `merge-info/staged/<branch>/` intact rather than copying it file by file and risking a
|
|
86
96
|
* half-moved merge. Only when the destination is an existing DIRECTORY do we descend and consider
|
|
87
97
|
* its children individually.
|
|
98
|
+
*
|
|
99
|
+
* `keepInPlace` is consulted at the TOP LEVEL only (`relative === ''`), because that is the scope
|
|
100
|
+
* `DotWebpieces.aiWritable()` assigns: a whole leaf of `.webpieces/` either lives in the worktree or
|
|
101
|
+
* it does not. A nested `pr-review/` under some other home is ordinary legacy state.
|
|
88
102
|
*/
|
|
89
|
-
drain(legacyRoot, targetRoot, relative, report) {
|
|
103
|
+
drain(legacyRoot, targetRoot, relative, report, keepInPlace) {
|
|
90
104
|
const from = path.join(legacyRoot, relative);
|
|
91
105
|
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
106
|
+
if (relative === '' && keepInPlace.has(entry.name))
|
|
107
|
+
continue;
|
|
92
108
|
const childRelative = path.join(relative, entry.name);
|
|
93
109
|
const source = path.join(legacyRoot, childRelative);
|
|
94
110
|
const destination = path.join(targetRoot, childRelative);
|
|
@@ -97,7 +113,7 @@ let StateDirMigrator = class StateDirMigrator {
|
|
|
97
113
|
continue;
|
|
98
114
|
}
|
|
99
115
|
if (entry.isDirectory() && fs.statSync(destination).isDirectory()) {
|
|
100
|
-
this.drain(legacyRoot, targetRoot, childRelative, report);
|
|
116
|
+
this.drain(legacyRoot, targetRoot, childRelative, report, keepInPlace);
|
|
101
117
|
this.removeIfEmpty(source);
|
|
102
118
|
continue;
|
|
103
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-dir-migration.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/state-dir-migration.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { toError } from './to-error';\n\n/**\n * What a migration did. Data-only (per CLAUDE.md: classes for data, explicit construction).\n *\n * `kept` is the important half: an entry that could NOT be moved because the destination already holds\n * something at that path. Nothing in `kept` is ever deleted — it stays where it is, for a human.\n */\nexport class StateMigrationReport {\n moved: string[] = [];\n kept: string[] = [];\n\n get movedAnything(): boolean {\n return this.moved.length > 0;\n }\n}\n\n/**\n * Moves a LEGACY per-worktree `.webpieces/` REAL DIRECTORY into the worktree's namespace inside the\n * primary clone (`<primary>/.webpieces/worktrees/<name>/`).\n *\n * NO SYMLINK IS EVER CREATED in its place — an earlier draft of this design planned one and this\n * comment still said so, which is worse than silence: it told the reader to expect an indirection\n * that does not exist. `DotWebpieces` resolves the namespace path EXPLICITLY (see its \"Why two\n * explicit methods and not a symlink\" section, which records why the link was rejected: lazy creation\n * with three failure modes, a Windows hazard, and `rename(2)` silently REPLACING the link). After a\n * migration the legacy `<worktree>/.webpieces/` is simply gone; nothing takes its place.\n *\n * WHY it must exist: those directories hold REAL in-flight state — a half-finished 3-point merge under\n * `merge-info/staged/<branch>/`, a written-but-not-yet-posted `pr-review/<branch>/review.json`. The\n * first invocation under the new scheme must not orphan them; a merge an agent is standing in the\n * middle of is not recoverable by re-running anything.\n *\n * WHY a plain move suffices: the destination namespace is created FOR this worktree, so it is empty in\n * the ordinary case and the whole tree moves in one `rename`. Where something is already there (a\n * repeat run, or an old PUBLISHED build having written to the legacy path again mid-transition), we\n * descend and move only what is free.\n *\n * SAFETY RULE, absolute: this never deletes or overwrites anything that holds content. If a\n * destination path is occupied, the legacy copy is LEFT WHERE IT IS and reported loudly on stderr.\n * Empty directories are removed as they drain, which is how a fully-migrated legacy `.webpieces/`\n * disappears on its own and lets the symlink be created.\n *\n * It is also the answer to the PUBLISHED-vs-LOCAL transition window. The `wp-*` bins and the hooks run\n * the PUBLISHED package, so for a while some invocations still create a REAL `<worktree>/.webpieces`\n * directory. Because this runs on the first state-dir resolution of EVERY new-code process, anything an\n * old-code invocation deposited is swept into the worktree namespace before any reader looks — and a\n * reader running old code finds it through the symlink either way. The two schemes converge instead of\n * splitting; nothing is lost in either direction.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class StateDirMigrator {\n /**\n * Drain `legacyDir` into `targetDir`. A no-op when they are the same directory or when the legacy\n * dir does not exist / is not a real directory (a symlink means migration already happened).\n */\n migrate(legacyDir: string, targetDir: string): StateMigrationReport {\n const report = new StateMigrationReport();\n if (path.resolve(legacyDir) === path.resolve(targetDir)) return report;\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!this.isRealDirectory(legacyDir)) return report;\n fs.mkdirSync(targetDir, { recursive: true });\n this.drain(legacyDir, targetDir, '', report);\n this.removeIfEmpty(legacyDir);\n } catch (err: unknown) {\n const error = toError(err);\n this.warn(`could not migrate ${legacyDir} into ${targetDir}: ${error.message}`);\n return report;\n }\n\n this.announce(legacyDir, targetDir, report);\n return report;\n }\n\n /**\n * Move everything under `<legacyRoot>/<relative>` to `<targetRoot>/<relative>`.\n *\n * A whole subtree whose destination is free moves in ONE `rename` — which is what keeps an\n * in-flight `merge-info/staged/<branch>/` intact rather than copying it file by file and risking a\n * half-moved merge. Only when the destination is an existing DIRECTORY do we descend and consider\n * its children individually.\n */\n private drain(legacyRoot: string, targetRoot: string, relative: string, report: StateMigrationReport): void {\n const from = path.join(legacyRoot, relative);\n for (const entry of fs.readdirSync(from, { withFileTypes: true })) {\n const childRelative = path.join(relative, entry.name);\n const source = path.join(legacyRoot, childRelative);\n const destination = path.join(targetRoot, childRelative);\n\n if (!fs.existsSync(destination)) {\n this.relocate(source, destination, childRelative, report);\n continue;\n }\n if (entry.isDirectory() && fs.statSync(destination).isDirectory()) {\n this.drain(legacyRoot, targetRoot, childRelative, report);\n this.removeIfEmpty(source);\n continue;\n }\n report.kept.push(childRelative);\n }\n }\n\n // Move one file or subtree to a free destination. `rename` first (atomic, instant, and the only\n // form that cannot half-move a merge); a cross-device rename falls back to copy-then-remove.\n private relocate(source: string, destination: string, relative: string, report: StateMigrationReport): void {\n fs.mkdirSync(path.dirname(destination), { recursive: true });\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.renameSync(source, destination);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n fs.cpSync(source, destination, { recursive: true });\n fs.rmSync(source, { recursive: true, force: true });\n }\n report.moved.push(relative);\n }\n\n // A real directory, not a symlink to one — `lstat` on purpose. A symlink here is the NEW scheme\n // already in place, and following it would make the migrator try to drain a directory into itself.\n private isRealDirectory(dir: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.lstatSync(dir).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // Remove a directory only when it is genuinely empty. Never recursive — a non-empty legacy dir is\n // state a human still needs.\n private removeIfEmpty(dir: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(dir)) return;\n if (fs.readdirSync(dir).length > 0) return;\n fs.rmdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // Migration is invisible by design when it goes cleanly; anything LEFT BEHIND must be shouted about,\n // because that is the only case where a human has to do something.\n private announce(legacyDir: string, targetDir: string, report: StateMigrationReport): void {\n if (report.kept.length === 0) return;\n this.warn(\n `${String(report.kept.length)} item(s) in ${legacyDir} could NOT move into ${targetDir} because ` +\n `that path is already occupied. NOTHING was deleted — resolve by hand if any of these still ` +\n `matters (an in-flight merge, or log history you have not read): ${report.kept.join(', ')}`,\n );\n }\n\n private warn(message: string): void {\n process.stderr.write(`[webpieces] .webpieces state migration: ${message}\\n`);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"state-dir-migration.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/state-dir-migration.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { toError } from './to-error';\n\n/**\n * What a migration did. Data-only (per CLAUDE.md: classes for data, explicit construction).\n *\n * `kept` is the important half: an entry that could NOT be moved because the destination already holds\n * something at that path. Nothing in `kept` is ever deleted — it stays where it is, for a human.\n */\nexport class StateMigrationReport {\n moved: string[] = [];\n kept: string[] = [];\n\n get movedAnything(): boolean {\n return this.moved.length > 0;\n }\n}\n\n/**\n * Moves a LEGACY per-worktree `.webpieces/` REAL DIRECTORY into the worktree's namespace inside the\n * primary clone (`<primary>/.webpieces/worktrees/<name>/`).\n *\n * NO SYMLINK IS EVER CREATED in its place — an earlier draft of this design planned one and this\n * comment still said so, which is worse than silence: it told the reader to expect an indirection\n * that does not exist. `DotWebpieces` resolves the namespace path EXPLICITLY (see its \"Why two\n * explicit methods and not a symlink\" section, which records why the link was rejected: lazy creation\n * with three failure modes, a Windows hazard, and `rename(2)` silently REPLACING the link). After a\n * migration the legacy `<worktree>/.webpieces/` is simply gone; nothing takes its place.\n *\n * WHY it must exist: those directories hold REAL in-flight state — above all a half-finished 3-point\n * merge under `merge-info/staged/<branch>/`. The first invocation under the new scheme must not orphan\n * them; a merge an agent is standing in the middle of is not recoverable by re-running anything.\n *\n * WHAT IT MUST NOT TOUCH: the `keepInPlace` leaves — `pr-review/` today. Those are not legacy at all;\n * `DotWebpieces.aiWritable()` resolves them to the worktree root ON PURPOSE, because that is the only\n * directory a worktree-isolated coding agent is permitted to write into. Draining one would delete the\n * live review directory out from under the agent writing it.\n *\n * WHY a plain move suffices: the destination namespace is created FOR this worktree, so it is empty in\n * the ordinary case and the whole tree moves in one `rename`. Where something is already there (a\n * repeat run, or an old PUBLISHED build having written to the legacy path again mid-transition), we\n * descend and move only what is free.\n *\n * SAFETY RULE, absolute: this never deletes or overwrites anything that holds content. If a\n * destination path is occupied, the legacy copy is LEFT WHERE IT IS and reported loudly on stderr.\n * Empty directories are removed as they drain, which is how a fully-migrated legacy `.webpieces/`\n * disappears on its own and lets the symlink be created.\n *\n * It is also the answer to the PUBLISHED-vs-LOCAL transition window. The `wp-*` bins and the hooks run\n * the PUBLISHED package, so for a while some invocations still create a REAL `<worktree>/.webpieces`\n * directory. Because this runs on the first state-dir resolution of EVERY new-code process, anything an\n * old-code invocation deposited is swept into the worktree namespace before any reader looks — and a\n * reader running old code finds it through the symlink either way. The two schemes converge instead of\n * splitting; nothing is lost in either direction.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class StateDirMigrator {\n /**\n * Drain `legacyDir` into `targetDir`. A no-op when they are the same directory or when the legacy\n * dir does not exist / is not a real directory (a symlink means migration already happened).\n *\n * `keepInPlace` names the TOP-LEVEL leaves of `legacyDir` that are NOT legacy — the ones\n * `DotWebpieces.aiWritable()` deliberately resolves to the worktree's own root because a\n * worktree-isolated coding agent cannot write anywhere else (today: `pr-review/`). Sweeping those\n * into the namespace would move a live directory out from under the agent that is mid-way through\n * writing it, so they are skipped and NOT reported as kept — nothing about them is unresolved.\n */\n migrate(legacyDir: string, targetDir: string, keepInPlace: readonly string[]): StateMigrationReport {\n const report = new StateMigrationReport();\n if (path.resolve(legacyDir) === path.resolve(targetDir)) return report;\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!this.isRealDirectory(legacyDir)) return report;\n fs.mkdirSync(targetDir, { recursive: true });\n this.drain(legacyDir, targetDir, '', report, new Set(keepInPlace));\n this.removeIfEmpty(legacyDir);\n } catch (err: unknown) {\n const error = toError(err);\n this.warn(`could not migrate ${legacyDir} into ${targetDir}: ${error.message}`);\n return report;\n }\n\n this.announce(legacyDir, targetDir, report);\n return report;\n }\n\n /**\n * Move everything under `<legacyRoot>/<relative>` to `<targetRoot>/<relative>`.\n *\n * A whole subtree whose destination is free moves in ONE `rename` — which is what keeps an\n * in-flight `merge-info/staged/<branch>/` intact rather than copying it file by file and risking a\n * half-moved merge. Only when the destination is an existing DIRECTORY do we descend and consider\n * its children individually.\n *\n * `keepInPlace` is consulted at the TOP LEVEL only (`relative === ''`), because that is the scope\n * `DotWebpieces.aiWritable()` assigns: a whole leaf of `.webpieces/` either lives in the worktree or\n * it does not. A nested `pr-review/` under some other home is ordinary legacy state.\n */\n private drain(\n legacyRoot: string, targetRoot: string, relative: string, report: StateMigrationReport,\n keepInPlace: ReadonlySet<string>,\n ): void {\n const from = path.join(legacyRoot, relative);\n for (const entry of fs.readdirSync(from, { withFileTypes: true })) {\n if (relative === '' && keepInPlace.has(entry.name)) continue;\n const childRelative = path.join(relative, entry.name);\n const source = path.join(legacyRoot, childRelative);\n const destination = path.join(targetRoot, childRelative);\n\n if (!fs.existsSync(destination)) {\n this.relocate(source, destination, childRelative, report);\n continue;\n }\n if (entry.isDirectory() && fs.statSync(destination).isDirectory()) {\n this.drain(legacyRoot, targetRoot, childRelative, report, keepInPlace);\n this.removeIfEmpty(source);\n continue;\n }\n report.kept.push(childRelative);\n }\n }\n\n // Move one file or subtree to a free destination. `rename` first (atomic, instant, and the only\n // form that cannot half-move a merge); a cross-device rename falls back to copy-then-remove.\n private relocate(source: string, destination: string, relative: string, report: StateMigrationReport): void {\n fs.mkdirSync(path.dirname(destination), { recursive: true });\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.renameSync(source, destination);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n fs.cpSync(source, destination, { recursive: true });\n fs.rmSync(source, { recursive: true, force: true });\n }\n report.moved.push(relative);\n }\n\n // A real directory, not a symlink to one — `lstat` on purpose. A symlink here is the NEW scheme\n // already in place, and following it would make the migrator try to drain a directory into itself.\n private isRealDirectory(dir: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.lstatSync(dir).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // Remove a directory only when it is genuinely empty. Never recursive — a non-empty legacy dir is\n // state a human still needs.\n private removeIfEmpty(dir: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(dir)) return;\n if (fs.readdirSync(dir).length > 0) return;\n fs.rmdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // Migration is invisible by design when it goes cleanly; anything LEFT BEHIND must be shouted about,\n // because that is the only case where a human has to do something.\n private announce(legacyDir: string, targetDir: string, report: StateMigrationReport): void {\n if (report.kept.length === 0) return;\n this.warn(\n `${String(report.kept.length)} item(s) in ${legacyDir} could NOT move into ${targetDir} because ` +\n `that path is already occupied. NOTHING was deleted — resolve by hand if any of these still ` +\n `matters (an in-flight merge, or log history you have not read): ${report.kept.join(', ')}`,\n );\n }\n\n private warn(message: string): void {\n process.stderr.write(`[webpieces] .webpieces state migration: ${message}\\n`);\n }\n}\n"]}
|
package/src/state-dir.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StateDirMigrator } from './state-dir-migration';
|
|
2
2
|
export declare const WORKTREE_STATE_DIR = "worktrees";
|
|
3
3
|
export declare const LOGS_STATE_DIR = "logs";
|
|
4
|
+
export declare const AI_WRITABLE_STATE_DIRS: readonly string[];
|
|
4
5
|
/**
|
|
5
6
|
* Data-only carrier for the two paths git is asked for. Per CLAUDE.md: classes for data.
|
|
6
7
|
*
|
|
@@ -16,12 +17,14 @@ export declare class GitDirs {
|
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
19
|
* WHERE a piece of `.webpieces/` state belongs — the ONE resolver every reader and writer must go
|
|
19
|
-
* through, with
|
|
20
|
+
* through, with THREE named methods because there are exactly three answers and a call site must DECLARE
|
|
20
21
|
* which one it means.
|
|
21
22
|
*
|
|
22
|
-
* dotWebpieces.shared(dir)
|
|
23
|
-
* dotWebpieces.local(dir)
|
|
24
|
-
*
|
|
23
|
+
* dotWebpieces.shared(dir) → <primary>/.webpieces (repo-wide facts)
|
|
24
|
+
* dotWebpieces.local(dir) → <primary>/.webpieces/worktrees/<name> (this worktree only, tooling-written)
|
|
25
|
+
* → <primary>/.webpieces (…in the primary clone)
|
|
26
|
+
* dotWebpieces.aiWritable(dir) → <worktree>/.webpieces (this worktree only, AI-WRITTEN)
|
|
27
|
+
* → <primary>/.webpieces (…in the primary clone)
|
|
25
28
|
*
|
|
26
29
|
* ─── The bug ───────────────────────────────────────────────────────────────────────────────────────
|
|
27
30
|
* `.webpieces/` is gitignored, and it was anchored at the directory holding webpieces.config.json —
|
|
@@ -54,8 +57,35 @@ export declare class GitDirs {
|
|
|
54
57
|
* per-worktree log has exactly ONE writer and cannot tear, and under this layout it already
|
|
55
58
|
* survives the worktree's deletion — recovery is one glob over
|
|
56
59
|
* `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.
|
|
57
|
-
* • merge-info/staged|merged/<branch>/ and its index.json,
|
|
58
|
-
*
|
|
60
|
+
* • merge-info/staged|merged/<branch>/ and its index.json, instruct-ai/, and every other per-tree
|
|
61
|
+
* scratch file that the TOOLING writes.
|
|
62
|
+
* aiWritable():
|
|
63
|
+
* • pr-review/<branch>/ — and ONLY that, today. See the section below.
|
|
64
|
+
*
|
|
65
|
+
* ─── Why aiWritable() is a third scope and not a spelling of local() ──────────────────────────────
|
|
66
|
+
* `pr-review/<branch>/` is the one state directory a CODING AGENT writes into with its own file-write
|
|
67
|
+
* tool: `review.json` is authored by the agent running the flow, and `review-<id>.json` by each reviewer
|
|
68
|
+
* subagent. A worktree-isolated agent may write ONLY inside its own worktree — its harness refuses any
|
|
69
|
+
* Write/Edit whose path is under the shared checkout, with "This agent is isolated in the worktree …;
|
|
70
|
+
* edit the worktree copy of this file instead". That refusal is not ours to relax.
|
|
71
|
+
*
|
|
72
|
+
* So `local()` — correct for everything the tooling writes — is UNREACHABLE for the two files the flow
|
|
73
|
+
* asks an agent to write. Stage ② printed a `<primary>/.webpieces/worktrees/<name>/pr-review/…` path,
|
|
74
|
+
* the agent's Write was refused at exactly that path, and the only way through was `cp`/`>` from Bash
|
|
75
|
+
* (which is NOT blocked, so the refusal looks arbitrary rather than structural). Three separate agents
|
|
76
|
+
* hit it, each concluded the tooling was printing a path it did not use, and each invented its own
|
|
77
|
+
* workaround. Worse, the same refusal lands on every reviewer subagent's `review-<id>.json`, and
|
|
78
|
+
* `wp-finish-upsert-pr` REFUSES the PR while a required checklist has no verdict — so this was a
|
|
79
|
+
* correctness bug, not an ergonomics one.
|
|
80
|
+
*
|
|
81
|
+
* The invariant, stated once: ANY `.webpieces/` path an AI AGENT is instructed to WRITE must resolve
|
|
82
|
+
* inside that agent's own working tree. `aiWritable()` is that rule, made greppable. Do not add a
|
|
83
|
+
* fourth scope; do not route a tooling-written file through this one.
|
|
84
|
+
*
|
|
85
|
+
* What it costs is what `local()` bought: a `pr-review/` under the worktree dies with the worktree.
|
|
86
|
+
* That is the right trade for THIS directory and only this one — a review is spent the moment the PR is
|
|
87
|
+
* posted, and `pnpm wp-review-upsert-pr` regenerates the whole directory from git in one run. An
|
|
88
|
+
* in-flight `merge-info/staged/<branch>/` is not regenerable, which is exactly why it stays on local().
|
|
59
89
|
*
|
|
60
90
|
* ─── The boundary that must not be blurred ─────────────────────────────────────────────────────────
|
|
61
91
|
* ONLY the gitignored `.webpieces/` STATE relocates. `webpieces.config.json` is TRACKED IN GIT and is
|
|
@@ -108,6 +138,17 @@ export declare class DotWebpieces {
|
|
|
108
138
|
local(startDir: string): string;
|
|
109
139
|
/** A path beneath this worktree's private state dir. */
|
|
110
140
|
localFile(startDir: string, ...segments: string[]): string;
|
|
141
|
+
/**
|
|
142
|
+
* State an AI AGENT writes with its own file-write tool: `<worktree>/.webpieces` — the worktree's
|
|
143
|
+
* OWN root, which is the only place a worktree-isolated agent is permitted to write. Identical to
|
|
144
|
+
* `shared()`/`local()` in the primary clone, so nothing changes for a repo without worktrees.
|
|
145
|
+
*
|
|
146
|
+
* See the "Why aiWritable() is a third scope" section above before adding a call site: this is for
|
|
147
|
+
* files the flow ASKS AN AGENT TO WRITE, not for tooling output that an agent merely reads.
|
|
148
|
+
*/
|
|
149
|
+
aiWritable(startDir: string): string;
|
|
150
|
+
/** A path beneath the AI-writable state dir. */
|
|
151
|
+
aiWritableFile(startDir: string, ...segments: string[]): string;
|
|
111
152
|
/**
|
|
112
153
|
* THE log directory — `<local()>/logs` — and the only place webpieces writes a `.log` or a log's
|
|
113
154
|
* detail files, in the primary clone and in every worktree namespace alike.
|
package/src/state-dir.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dotWebpieces = exports.DotWebpieces = exports.GitDirs = exports.LOGS_STATE_DIR = exports.WORKTREE_STATE_DIR = void 0;
|
|
3
|
+
exports.dotWebpieces = exports.DotWebpieces = exports.GitDirs = exports.AI_WRITABLE_STATE_DIRS = exports.LOGS_STATE_DIR = exports.WORKTREE_STATE_DIR = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const fs = tslib_1.__importStar(require("fs"));
|
|
@@ -28,6 +28,12 @@ exports.WORKTREE_STATE_DIR = 'worktrees';
|
|
|
28
28
|
// <sessionId>-<agentId|coordinator>-<hook>.log — because Claude Code runs all matching PreToolUse
|
|
29
29
|
// hooks IN PARALLEL and subagents/windows share a tree, so writers must never share a file.
|
|
30
30
|
exports.LOGS_STATE_DIR = 'logs';
|
|
31
|
+
// The leaves of `.webpieces/` that resolve through `aiWritable()` and therefore live in the WORKTREE's
|
|
32
|
+
// own root rather than in the primary clone's namespace. Named here because TWO things must agree about
|
|
33
|
+
// them: `aiWritable()`, which puts them there, and `StateDirMigrator`, which must NOT sweep them out
|
|
34
|
+
// again (its whole job is draining `<worktree>/.webpieces` into the namespace, and these are the one
|
|
35
|
+
// part of that directory that is live rather than legacy).
|
|
36
|
+
exports.AI_WRITABLE_STATE_DIRS = [constants_1.PR_REVIEW_DIR];
|
|
31
37
|
// git prints the shared git dir as `<primary>/.git` for a conventional clone. Anything else (a bare
|
|
32
38
|
// repo, `--separate-git-dir`) is a layout we decline to derive a working tree from.
|
|
33
39
|
const GIT_DIR_NAME = '.git';
|
|
@@ -52,12 +58,14 @@ class GitDirs {
|
|
|
52
58
|
exports.GitDirs = GitDirs;
|
|
53
59
|
/**
|
|
54
60
|
* WHERE a piece of `.webpieces/` state belongs — the ONE resolver every reader and writer must go
|
|
55
|
-
* through, with
|
|
61
|
+
* through, with THREE named methods because there are exactly three answers and a call site must DECLARE
|
|
56
62
|
* which one it means.
|
|
57
63
|
*
|
|
58
|
-
* dotWebpieces.shared(dir)
|
|
59
|
-
* dotWebpieces.local(dir)
|
|
60
|
-
*
|
|
64
|
+
* dotWebpieces.shared(dir) → <primary>/.webpieces (repo-wide facts)
|
|
65
|
+
* dotWebpieces.local(dir) → <primary>/.webpieces/worktrees/<name> (this worktree only, tooling-written)
|
|
66
|
+
* → <primary>/.webpieces (…in the primary clone)
|
|
67
|
+
* dotWebpieces.aiWritable(dir) → <worktree>/.webpieces (this worktree only, AI-WRITTEN)
|
|
68
|
+
* → <primary>/.webpieces (…in the primary clone)
|
|
61
69
|
*
|
|
62
70
|
* ─── The bug ───────────────────────────────────────────────────────────────────────────────────────
|
|
63
71
|
* `.webpieces/` is gitignored, and it was anchored at the directory holding webpieces.config.json —
|
|
@@ -90,8 +98,35 @@ exports.GitDirs = GitDirs;
|
|
|
90
98
|
* per-worktree log has exactly ONE writer and cannot tear, and under this layout it already
|
|
91
99
|
* survives the worktree's deletion — recovery is one glob over
|
|
92
100
|
* `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.
|
|
93
|
-
* • merge-info/staged|merged/<branch>/ and its index.json,
|
|
94
|
-
*
|
|
101
|
+
* • merge-info/staged|merged/<branch>/ and its index.json, instruct-ai/, and every other per-tree
|
|
102
|
+
* scratch file that the TOOLING writes.
|
|
103
|
+
* aiWritable():
|
|
104
|
+
* • pr-review/<branch>/ — and ONLY that, today. See the section below.
|
|
105
|
+
*
|
|
106
|
+
* ─── Why aiWritable() is a third scope and not a spelling of local() ──────────────────────────────
|
|
107
|
+
* `pr-review/<branch>/` is the one state directory a CODING AGENT writes into with its own file-write
|
|
108
|
+
* tool: `review.json` is authored by the agent running the flow, and `review-<id>.json` by each reviewer
|
|
109
|
+
* subagent. A worktree-isolated agent may write ONLY inside its own worktree — its harness refuses any
|
|
110
|
+
* Write/Edit whose path is under the shared checkout, with "This agent is isolated in the worktree …;
|
|
111
|
+
* edit the worktree copy of this file instead". That refusal is not ours to relax.
|
|
112
|
+
*
|
|
113
|
+
* So `local()` — correct for everything the tooling writes — is UNREACHABLE for the two files the flow
|
|
114
|
+
* asks an agent to write. Stage ② printed a `<primary>/.webpieces/worktrees/<name>/pr-review/…` path,
|
|
115
|
+
* the agent's Write was refused at exactly that path, and the only way through was `cp`/`>` from Bash
|
|
116
|
+
* (which is NOT blocked, so the refusal looks arbitrary rather than structural). Three separate agents
|
|
117
|
+
* hit it, each concluded the tooling was printing a path it did not use, and each invented its own
|
|
118
|
+
* workaround. Worse, the same refusal lands on every reviewer subagent's `review-<id>.json`, and
|
|
119
|
+
* `wp-finish-upsert-pr` REFUSES the PR while a required checklist has no verdict — so this was a
|
|
120
|
+
* correctness bug, not an ergonomics one.
|
|
121
|
+
*
|
|
122
|
+
* The invariant, stated once: ANY `.webpieces/` path an AI AGENT is instructed to WRITE must resolve
|
|
123
|
+
* inside that agent's own working tree. `aiWritable()` is that rule, made greppable. Do not add a
|
|
124
|
+
* fourth scope; do not route a tooling-written file through this one.
|
|
125
|
+
*
|
|
126
|
+
* What it costs is what `local()` bought: a `pr-review/` under the worktree dies with the worktree.
|
|
127
|
+
* That is the right trade for THIS directory and only this one — a review is spent the moment the PR is
|
|
128
|
+
* posted, and `pnpm wp-review-upsert-pr` regenerates the whole directory from git in one run. An
|
|
129
|
+
* in-flight `merge-info/staged/<branch>/` is not regenerable, which is exactly why it stays on local().
|
|
95
130
|
*
|
|
96
131
|
* ─── The boundary that must not be blurred ─────────────────────────────────────────────────────────
|
|
97
132
|
* ONLY the gitignored `.webpieces/` STATE relocates. `webpieces.config.json` is TRACKED IN GIT and is
|
|
@@ -163,6 +198,25 @@ let DotWebpieces = class DotWebpieces {
|
|
|
163
198
|
localFile(startDir, ...segments) {
|
|
164
199
|
return path.join(this.local(startDir), ...segments);
|
|
165
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* State an AI AGENT writes with its own file-write tool: `<worktree>/.webpieces` — the worktree's
|
|
203
|
+
* OWN root, which is the only place a worktree-isolated agent is permitted to write. Identical to
|
|
204
|
+
* `shared()`/`local()` in the primary clone, so nothing changes for a repo without worktrees.
|
|
205
|
+
*
|
|
206
|
+
* See the "Why aiWritable() is a third scope" section above before adding a call site: this is for
|
|
207
|
+
* files the flow ASKS AN AGENT TO WRITE, not for tooling output that an agent merely reads.
|
|
208
|
+
*/
|
|
209
|
+
aiWritable(startDir) {
|
|
210
|
+
const toplevel = this.treeRoot(startDir);
|
|
211
|
+
// Fails CLOSED to shared() when git cannot answer, on the same terms as every other path here.
|
|
212
|
+
if (toplevel === null)
|
|
213
|
+
return this.shared(startDir);
|
|
214
|
+
return path.join(toplevel, constants_1.WEBPIECES_TMP_DIR);
|
|
215
|
+
}
|
|
216
|
+
/** A path beneath the AI-writable state dir. */
|
|
217
|
+
aiWritableFile(startDir, ...segments) {
|
|
218
|
+
return path.join(this.aiWritable(startDir), ...segments);
|
|
219
|
+
}
|
|
166
220
|
/**
|
|
167
221
|
* THE log directory — `<local()>/logs` — and the only place webpieces writes a `.log` or a log's
|
|
168
222
|
* detail files, in the primary clone and in every worktree namespace alike.
|
|
@@ -269,7 +323,7 @@ let DotWebpieces = class DotWebpieces {
|
|
|
269
323
|
const toplevel = this.treeRoot(startDir);
|
|
270
324
|
if (toplevel === null)
|
|
271
325
|
return;
|
|
272
|
-
this.migrator.migrate(this.legacyDir(toplevel), target);
|
|
326
|
+
this.migrator.migrate(this.legacyDir(toplevel), target, exports.AI_WRITABLE_STATE_DIRS);
|
|
273
327
|
}
|
|
274
328
|
// One `git rev-parse <flag>`, resolved to an absolute path (git prints a bare `.git`, relative to
|
|
275
329
|
// the tree, in the primary clone, and an absolute path from a linked worktree).
|
package/src/state-dir.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-dir.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/state-dir.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WEBPIECES_TMP_DIR } from './constants';\nimport { StateDirMigrator } from './state-dir-migration';\n\n// The per-worktree namespace inside the primary clone's `.webpieces/`. A LINKED worktree's local state\n// lives at `<primary>/.webpieces/worktrees/<worktreeName>/`; the primary clone keeps using\n// `<primary>/.webpieces/` directly, exactly as it always has.\nexport const WORKTREE_STATE_DIR = 'worktrees';\n\n// The ONE and ONLY directory webpieces writes observability state into — `<state>/logs/`, for the\n// primary clone and for each worktree namespace alike. There is no second state directory and no\n// split: the L1 binary's logs, the L0 sh shim's log, and the rejection DETAIL files (which are not\n// `.log`s but belong to the log that indexes them) are all here.\n//\n// It used to be two directories — `hooks/`, holding the binary's logs mixed in with dated\n// `hooks/<YYYY-MM-DD>/writeInfo-*.md` rejection details, and `logs/`, where the sh shim wrote — so\n// \"where are the logs?\" had two answers and neither was complete. `hooks/` is gone entirely; nothing\n// reads or writes it.\n//\n// Every writer resolves its DIRECTORY through dotWebpieces.logs()/logsFile(), so the layout cannot\n// drift apart again. What sits INSIDE it is ai-hook-rules' business (see its log-streams.ts and\n// LogStream): one directory per LAYER, and inside that one file per WRITER, named\n// <sessionId>-<agentId|coordinator>-<hook>.log — because Claude Code runs all matching PreToolUse\n// hooks IN PARALLEL and subagents/windows share a tree, so writers must never share a file.\nexport const LOGS_STATE_DIR = 'logs';\n\n\n// git prints the shared git dir as `<primary>/.git` for a conventional clone. Anything else (a bare\n// repo, `--separate-git-dir`) is a layout we decline to derive a working tree from.\nconst GIT_DIR_NAME = '.git';\n\n/**\n * Data-only carrier for the two paths git is asked for. Per CLAUDE.md: classes for data.\n *\n * `gitDir` is the PER-WORKTREE git dir (`<primary>/.git/worktrees/<name>` in a linked worktree,\n * `<primary>/.git` in the primary clone). `commonDir` is the SHARED one — always `<primary>/.git`.\n * They differ if and only if this is a linked worktree; that is git's own canonical test.\n */\nexport class GitDirs {\n readonly gitDir: string;\n readonly commonDir: string;\n\n constructor(gitDir: string, commonDir: string) {\n this.gitDir = gitDir;\n this.commonDir = commonDir;\n }\n\n get isLinkedWorktree(): boolean {\n return path.resolve(this.gitDir) !== path.resolve(this.commonDir);\n }\n}\n\n/**\n * WHERE a piece of `.webpieces/` state belongs — the ONE resolver every reader and writer must go\n * through, with TWO named methods because there are exactly two answers and a call site must DECLARE\n * which one it means.\n *\n * dotWebpieces.shared(dir) → <primary>/.webpieces (repo-wide facts)\n * dotWebpieces.local(dir) → <primary>/.webpieces/worktrees/<name> (this worktree only)\n * → <primary>/.webpieces (…in the primary clone)\n *\n * ─── The bug ───────────────────────────────────────────────────────────────────────────────────────\n * `.webpieces/` is gitignored, and it was anchored at the directory holding webpieces.config.json —\n * which in a linked worktree is the WORKTREE. So a repo with seven worktrees had SEVEN independent\n * copies of files that describe the WHOLE REPO. `merged-branches.json` holds verdicts for every branch\n * AND every worktree in the repo; N copies is N divergent truths, and the guards read them as fact.\n * Observed in the field: branch-creation-guard asserted \"8 parked local branches\" while\n * `git branch --list` showed ONE — it was reading a cache written before deletions performed from a\n * DIFFERENT worktree — and it then blocked a legitimate `git worktree add` on that fiction.\n *\n * ─── Why two explicit methods and not a symlink ────────────────────────────────────────────────────\n * A `<worktree>/.webpieces` → `<primary>/…/worktrees/<name>` symlink would have left every call site\n * untouched, which is seductive and wrong. Nothing hooks `git worktree add`, so the link needs LAZY\n * creation that also has to handle \"link already exists\", \"link points somewhere else\", and \"a real\n * directory is already there\" — invisible filesystem magic with a Windows failure mode. Worse, the safe\n * way to write a shared file under concurrency is temp-file-then-`rename()`, and `rename(2)` acts on\n * the PATH, not the link: it REPLACES a symlink with a real file. That silently works for one writer\n * and diverges for everyone else — the exact bug being fixed, but invisible. An explicit call is\n * greppable, testable, and forces each site to say which scope it means.\n *\n * ─── Which scope is which (the scope assignment is deliberate, not incidental) ─────────────────────\n * shared():\n * • merged-branches.json — verdicts for every branch and worktree in the repo. Atomically written.\n * • main-sync-status.json AND main-sync.lock.json — there is one `main` and one `.git`, so there is\n * one refresher. A lock inside a per-worktree directory locks nothing.\n * local():\n * • logs/*.log, INCLUDING branch-mutations.log. A shared append-only log genuinely corrupts:\n * `O_APPEND` writes are indivisible only under PIPE_BUF, which is 512 bytes on macOS, and a\n * `recover=git worktree add -b <branch> <abs-path> <tag>` line with real paths exceeds that. A\n * per-worktree log has exactly ONE writer and cannot tear, and under this layout it already\n * survives the worktree's deletion — recovery is one glob over\n * `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.\n * • merge-info/staged|merged/<branch>/ and its index.json, pr-review/<branch>/, instruct-ai/, and\n * every other per-tree scratch file.\n *\n * ─── The boundary that must not be blurred ─────────────────────────────────────────────────────────\n * ONLY the gitignored `.webpieces/` STATE relocates. `webpieces.config.json` is TRACKED IN GIT and is\n * therefore part of the BRANCH: a branch may legitimately change its own rules and that must keep\n * working. Config resolution is untouched — still per-worktree, via findConfigFile /\n * RepoRootFinder.resolveRepoRoot. Nothing in this class reads or moves config.\n *\n * ─── Why `--git-dir` / `--git-common-dir`, and not one of the existing services ────────────────────\n * They are git's own answers, from any subdirectory, in one cheap local call, with no `.git`-file\n * parsing by hand (which gets `--separate-git-dir` and submodules wrong). Two existing mechanisms do\n * distinguish primary from linked, and neither is the right authority here:\n * • `WorktreeService` answers \"what worktrees exist and what do they hold\" — a repo-wide ENUMERATION\n * (`git worktree list --porcelain`) for the caps and the reaper. Using it for a path lookup would\n * run an enumeration on the hook's blocking path for every state access, and it fails SOFT to `[]`,\n * which here would read as \"there is no primary clone\" on exactly the degraded repo where the\n * answer matters most. It also does not expose the worktree's git NAME, which is the namespace key.\n * • `EffectiveTreeResolver` (#524) answers \"which tree does this COMMAND act on\" — it takes a command\n * string and is a policy input to the bash guards, not a filesystem-path resolver.\n * Both remain authoritative for their own questions. This asks the narrowest one — two path strings —\n * and lives in rules-config, UNDER both, which is where a primitive that pr-gate, ai-hook-rules and\n * code-rules all need has to sit.\n *\n * Fails CLOSED to the pre-change behaviour: when git cannot answer, every path here collapses to\n * `<startDir-root>/.webpieces` exactly as before. Degrading to merely-suboptimal beats throwing on a\n * hook's blocking path.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DotWebpieces {\n // treeRoot → git's answer. One `git rev-parse` pair per root per process; every later path lookup\n // in that invocation is a Map hit.\n private readonly gitDirsByRoot = new Map<string, GitDirs | null>();\n // startDir → `git rev-parse --show-toplevel`, cached on the same terms as gitDirsByRoot.\n private readonly treeRootByDir = new Map<string, string | null>();\n // Roots whose legacy per-worktree `.webpieces/` has already been considered for migration.\n private readonly migrated = new Set<string>();\n\n constructor(private readonly migrator: StateDirMigrator = new StateDirMigrator()) {}\n\n /**\n * REPO-WIDE state: `<primary>/.webpieces`. Use ONLY for facts about the repo rather than about one\n * worktree — today that is merged-branches.json and the main-sync status + lock. Identical from\n * every worktree, and never behind an indirection, so an atomic `rename()` into it is safe.\n */\n shared(startDir: string): string {\n return path.join(this.primaryRoot(startDir), WEBPIECES_TMP_DIR);\n }\n\n /** A path beneath the repo-wide state dir. */\n sharedFile(startDir: string, ...segments: string[]): string {\n return path.join(this.shared(startDir), ...segments);\n }\n\n /**\n * THIS WORKTREE's private state: `<primary>/.webpieces/worktrees/<name>` for a linked worktree, and\n * `<primary>/.webpieces` for the primary clone, which keeps its state exactly where it has always\n * been. Fully isolated — two worktrees never write the same path, so nothing here needs a lock.\n *\n * The first call for a linked worktree also MIGRATES a legacy real `<worktree>/.webpieces/`\n * directory into the namespace, so in-flight merge / pr-review state written under the old scheme\n * (or by an older PUBLISHED build during the transition) is picked up rather than orphaned.\n */\n local(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return this.shared(startDir);\n\n const target = path.join(this.shared(startDir), WORKTREE_STATE_DIR, path.basename(dirs.gitDir));\n this.migrateOnce(startDir, target);\n return target;\n }\n\n /** A path beneath this worktree's private state dir. */\n localFile(startDir: string, ...segments: string[]): string {\n return path.join(this.local(startDir), ...segments);\n }\n\n /**\n * THE log directory — `<local()>/logs` — and the only place webpieces writes a `.log` or a log's\n * detail files, in the primary clone and in every worktree namespace alike.\n *\n * There is NO migration here, deliberately. A lazy once-per-process relocation of an older\n * release's `hooks/*.log` used to run on this path, and it was itself the defect it claimed to\n * cure: it only fires in the trees a process happens to re-enter, so it guarantees two answers to\n * \"where are the logs\" indefinitely rather than converging on one. New writes go to `logs/` and\n * nothing else; a stale `hooks/` left by an older release is inert and can be deleted by hand.\n */\n logs(startDir: string): string {\n return path.join(this.local(startDir), LOGS_STATE_DIR);\n }\n\n /** A path beneath the log directory — `dotWebpieces.logsFile(root, CALLS_STREAM, writerFile)`. */\n logsFile(startDir: string, ...segments: string[]): string {\n return path.join(this.logs(startDir), ...segments);\n }\n\n /** True when `startDir` sits in a LINKED worktree rather than the primary clone. */\n isLinkedWorktree(startDir: string): boolean {\n const dirs = this.gitDirs(startDir);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * git's two dir answers for `startDir`, memoized — null when it is not a git repo / git is\n * unavailable.\n *\n * PUBLIC because TREE IDENTITY is decided from these two strings and nothing else. `commonDir`\n * is the same for every checkout of ONE repo, so `commonDir(a) === commonDir(b)` is \"same repo,\n * different placement\" — the test that tells a linked worktree (ours, wherever it sits, including\n * inside the repo at `.claude/worktrees/**`) from a nested clone under `repositories/**` (not\n * ours). `gitDirs.isLinkedWorktree` then separates primary from linked. See\n * `EffectiveTreeResolver.classify`, the caller that runs this on the hook's blocking path, which is\n * why it goes through this cache rather than spawning its own `rev-parse`.\n */\n gitDirs(startDir: string): GitDirs | null {\n const cached = this.gitDirsByRoot.get(startDir);\n if (cached !== undefined) return cached;\n\n const gitDir = this.revParse(startDir, '--git-dir');\n const commonDir = this.revParse(startDir, '--git-common-dir');\n const dirs = gitDir === null || commonDir === null ? null : new GitDirs(gitDir, commonDir);\n this.gitDirsByRoot.set(startDir, dirs);\n return dirs;\n }\n\n /**\n * The WORKING TREE root containing `startDir` (`git rev-parse --show-toplevel`), memoized, or null\n * when `startDir` is not in a git repo. In a linked worktree this is the WORKTREE's own root, which\n * is exactly what a caller judging \"which checkout am I standing in\" needs.\n *\n * THE ONE tree-root resolver. There used to be three byte-identical `spawnSync … --show-toplevel`\n * copies — this one, `RepoRootFinder`'s and `effective-tree.ts`'s — each commenting that it\n * \"mirrors\" one of the others, and one of those comments was already stale. Both copies are deleted;\n * everything that needs a tree root calls this. Do not add a fourth.\n */\n treeRoot(startDir: string): string | null {\n const cached = this.treeRootByDir.get(startDir);\n if (cached !== undefined) return cached;\n // `status !== 0` IS the expected \"not a repo\" answer (spawnSync does not throw on a non-zero\n // exit), so nothing here swallows a real git crash.\n const root = this.revParse(startDir, '--show-toplevel');\n this.treeRootByDir.set(startDir, root);\n return root;\n }\n\n /**\n * git's own name for this linked worktree — the basename of `<primary>/.git/worktrees/<name>`, and\n * the namespace key under `worktrees/`. Empty for the primary clone. git's name rather than the\n * directory's basename, so two worktrees checked out into same-named directories under different\n * parents cannot collide.\n */\n worktreeName(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return '';\n return path.basename(dirs.gitDir);\n }\n\n /** The primary clone's root, from any worktree. Falls back to git's toplevel-less best guess. */\n primaryRoot(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null) return startDir;\n if (path.basename(dirs.commonDir) !== GIT_DIR_NAME) return startDir;\n const primary = path.dirname(dirs.commonDir);\n return fs.existsSync(primary) ? primary : startDir;\n }\n\n /**\n * The PRE-change location, `<treeRoot>/.webpieces` — what every call site used to compute. Public\n * because the migrator and its specs must be able to name the thing being migrated FROM, and\n * because the transition-window fallback readers need it.\n */\n legacyDir(treeRoot: string): string {\n return path.join(treeRoot, WEBPIECES_TMP_DIR);\n }\n\n // Drain a legacy per-worktree `.webpieces/` into this worktree's namespace, at most once per tree\n // per process. Migration is idempotent, but it touches the filesystem on the hook's blocking path.\n private migrateOnce(startDir: string, target: string): void {\n // Guard on the CHEAP key first. `local()` is called many times per invocation, and resolving the\n // worktree toplevel costs a `git rev-parse` — doing that before the once-check would put a\n // process spawn on the hook's blocking path for every single state-path lookup.\n if (this.migrated.has(startDir)) return;\n this.migrated.add(startDir);\n const toplevel = this.treeRoot(startDir);\n if (toplevel === null) return;\n this.migrator.migrate(this.legacyDir(toplevel), target);\n }\n\n // One `git rev-parse <flag>`, resolved to an absolute path (git prints a bare `.git`, relative to\n // the tree, in the primary clone, and an absolute path from a linked worktree).\n private revParse(cwd: string, flag: string): string | null {\n const result = spawnSync('git', ['-C', cwd, 'rev-parse', flag], { encoding: 'utf8' });\n if (result.status !== 0) return null;\n const printed = (result.stdout ?? '').trim();\n if (printed === '') return null;\n return path.resolve(cwd, printed);\n }\n}\n\n// Process-wide instance for the many non-DI call sites (hooks, detached refreshers, wp-* bins, eslint\n// rules). Sharing one instance is what makes the git-resolution cache and the once-per-tree migration\n// worth having; inversify still injects the singleton wherever a container is in play.\nexport const dotWebpieces = new DotWebpieces();\n"]}
|
|
1
|
+
{"version":3,"file":"state-dir.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/state-dir.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { PR_REVIEW_DIR, WEBPIECES_TMP_DIR } from './constants';\nimport { StateDirMigrator } from './state-dir-migration';\n\n// The per-worktree namespace inside the primary clone's `.webpieces/`. A LINKED worktree's local state\n// lives at `<primary>/.webpieces/worktrees/<worktreeName>/`; the primary clone keeps using\n// `<primary>/.webpieces/` directly, exactly as it always has.\nexport const WORKTREE_STATE_DIR = 'worktrees';\n\n// The ONE and ONLY directory webpieces writes observability state into — `<state>/logs/`, for the\n// primary clone and for each worktree namespace alike. There is no second state directory and no\n// split: the L1 binary's logs, the L0 sh shim's log, and the rejection DETAIL files (which are not\n// `.log`s but belong to the log that indexes them) are all here.\n//\n// It used to be two directories — `hooks/`, holding the binary's logs mixed in with dated\n// `hooks/<YYYY-MM-DD>/writeInfo-*.md` rejection details, and `logs/`, where the sh shim wrote — so\n// \"where are the logs?\" had two answers and neither was complete. `hooks/` is gone entirely; nothing\n// reads or writes it.\n//\n// Every writer resolves its DIRECTORY through dotWebpieces.logs()/logsFile(), so the layout cannot\n// drift apart again. What sits INSIDE it is ai-hook-rules' business (see its log-streams.ts and\n// LogStream): one directory per LAYER, and inside that one file per WRITER, named\n// <sessionId>-<agentId|coordinator>-<hook>.log — because Claude Code runs all matching PreToolUse\n// hooks IN PARALLEL and subagents/windows share a tree, so writers must never share a file.\nexport const LOGS_STATE_DIR = 'logs';\n\n// The leaves of `.webpieces/` that resolve through `aiWritable()` and therefore live in the WORKTREE's\n// own root rather than in the primary clone's namespace. Named here because TWO things must agree about\n// them: `aiWritable()`, which puts them there, and `StateDirMigrator`, which must NOT sweep them out\n// again (its whole job is draining `<worktree>/.webpieces` into the namespace, and these are the one\n// part of that directory that is live rather than legacy).\nexport const AI_WRITABLE_STATE_DIRS: readonly string[] = [PR_REVIEW_DIR];\n\n\n// git prints the shared git dir as `<primary>/.git` for a conventional clone. Anything else (a bare\n// repo, `--separate-git-dir`) is a layout we decline to derive a working tree from.\nconst GIT_DIR_NAME = '.git';\n\n/**\n * Data-only carrier for the two paths git is asked for. Per CLAUDE.md: classes for data.\n *\n * `gitDir` is the PER-WORKTREE git dir (`<primary>/.git/worktrees/<name>` in a linked worktree,\n * `<primary>/.git` in the primary clone). `commonDir` is the SHARED one — always `<primary>/.git`.\n * They differ if and only if this is a linked worktree; that is git's own canonical test.\n */\nexport class GitDirs {\n readonly gitDir: string;\n readonly commonDir: string;\n\n constructor(gitDir: string, commonDir: string) {\n this.gitDir = gitDir;\n this.commonDir = commonDir;\n }\n\n get isLinkedWorktree(): boolean {\n return path.resolve(this.gitDir) !== path.resolve(this.commonDir);\n }\n}\n\n/**\n * WHERE a piece of `.webpieces/` state belongs — the ONE resolver every reader and writer must go\n * through, with THREE named methods because there are exactly three answers and a call site must DECLARE\n * which one it means.\n *\n * dotWebpieces.shared(dir) → <primary>/.webpieces (repo-wide facts)\n * dotWebpieces.local(dir) → <primary>/.webpieces/worktrees/<name> (this worktree only, tooling-written)\n * → <primary>/.webpieces (…in the primary clone)\n * dotWebpieces.aiWritable(dir) → <worktree>/.webpieces (this worktree only, AI-WRITTEN)\n * → <primary>/.webpieces (…in the primary clone)\n *\n * ─── The bug ───────────────────────────────────────────────────────────────────────────────────────\n * `.webpieces/` is gitignored, and it was anchored at the directory holding webpieces.config.json —\n * which in a linked worktree is the WORKTREE. So a repo with seven worktrees had SEVEN independent\n * copies of files that describe the WHOLE REPO. `merged-branches.json` holds verdicts for every branch\n * AND every worktree in the repo; N copies is N divergent truths, and the guards read them as fact.\n * Observed in the field: branch-creation-guard asserted \"8 parked local branches\" while\n * `git branch --list` showed ONE — it was reading a cache written before deletions performed from a\n * DIFFERENT worktree — and it then blocked a legitimate `git worktree add` on that fiction.\n *\n * ─── Why two explicit methods and not a symlink ────────────────────────────────────────────────────\n * A `<worktree>/.webpieces` → `<primary>/…/worktrees/<name>` symlink would have left every call site\n * untouched, which is seductive and wrong. Nothing hooks `git worktree add`, so the link needs LAZY\n * creation that also has to handle \"link already exists\", \"link points somewhere else\", and \"a real\n * directory is already there\" — invisible filesystem magic with a Windows failure mode. Worse, the safe\n * way to write a shared file under concurrency is temp-file-then-`rename()`, and `rename(2)` acts on\n * the PATH, not the link: it REPLACES a symlink with a real file. That silently works for one writer\n * and diverges for everyone else — the exact bug being fixed, but invisible. An explicit call is\n * greppable, testable, and forces each site to say which scope it means.\n *\n * ─── Which scope is which (the scope assignment is deliberate, not incidental) ─────────────────────\n * shared():\n * • merged-branches.json — verdicts for every branch and worktree in the repo. Atomically written.\n * • main-sync-status.json AND main-sync.lock.json — there is one `main` and one `.git`, so there is\n * one refresher. A lock inside a per-worktree directory locks nothing.\n * local():\n * • logs/*.log, INCLUDING branch-mutations.log. A shared append-only log genuinely corrupts:\n * `O_APPEND` writes are indivisible only under PIPE_BUF, which is 512 bytes on macOS, and a\n * `recover=git worktree add -b <branch> <abs-path> <tag>` line with real paths exceeds that. A\n * per-worktree log has exactly ONE writer and cannot tear, and under this layout it already\n * survives the worktree's deletion — recovery is one glob over\n * `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.\n * • merge-info/staged|merged/<branch>/ and its index.json, instruct-ai/, and every other per-tree\n * scratch file that the TOOLING writes.\n * aiWritable():\n * • pr-review/<branch>/ — and ONLY that, today. See the section below.\n *\n * ─── Why aiWritable() is a third scope and not a spelling of local() ──────────────────────────────\n * `pr-review/<branch>/` is the one state directory a CODING AGENT writes into with its own file-write\n * tool: `review.json` is authored by the agent running the flow, and `review-<id>.json` by each reviewer\n * subagent. A worktree-isolated agent may write ONLY inside its own worktree — its harness refuses any\n * Write/Edit whose path is under the shared checkout, with \"This agent is isolated in the worktree …;\n * edit the worktree copy of this file instead\". That refusal is not ours to relax.\n *\n * So `local()` — correct for everything the tooling writes — is UNREACHABLE for the two files the flow\n * asks an agent to write. Stage ② printed a `<primary>/.webpieces/worktrees/<name>/pr-review/…` path,\n * the agent's Write was refused at exactly that path, and the only way through was `cp`/`>` from Bash\n * (which is NOT blocked, so the refusal looks arbitrary rather than structural). Three separate agents\n * hit it, each concluded the tooling was printing a path it did not use, and each invented its own\n * workaround. Worse, the same refusal lands on every reviewer subagent's `review-<id>.json`, and\n * `wp-finish-upsert-pr` REFUSES the PR while a required checklist has no verdict — so this was a\n * correctness bug, not an ergonomics one.\n *\n * The invariant, stated once: ANY `.webpieces/` path an AI AGENT is instructed to WRITE must resolve\n * inside that agent's own working tree. `aiWritable()` is that rule, made greppable. Do not add a\n * fourth scope; do not route a tooling-written file through this one.\n *\n * What it costs is what `local()` bought: a `pr-review/` under the worktree dies with the worktree.\n * That is the right trade for THIS directory and only this one — a review is spent the moment the PR is\n * posted, and `pnpm wp-review-upsert-pr` regenerates the whole directory from git in one run. An\n * in-flight `merge-info/staged/<branch>/` is not regenerable, which is exactly why it stays on local().\n *\n * ─── The boundary that must not be blurred ─────────────────────────────────────────────────────────\n * ONLY the gitignored `.webpieces/` STATE relocates. `webpieces.config.json` is TRACKED IN GIT and is\n * therefore part of the BRANCH: a branch may legitimately change its own rules and that must keep\n * working. Config resolution is untouched — still per-worktree, via findConfigFile /\n * RepoRootFinder.resolveRepoRoot. Nothing in this class reads or moves config.\n *\n * ─── Why `--git-dir` / `--git-common-dir`, and not one of the existing services ────────────────────\n * They are git's own answers, from any subdirectory, in one cheap local call, with no `.git`-file\n * parsing by hand (which gets `--separate-git-dir` and submodules wrong). Two existing mechanisms do\n * distinguish primary from linked, and neither is the right authority here:\n * • `WorktreeService` answers \"what worktrees exist and what do they hold\" — a repo-wide ENUMERATION\n * (`git worktree list --porcelain`) for the caps and the reaper. Using it for a path lookup would\n * run an enumeration on the hook's blocking path for every state access, and it fails SOFT to `[]`,\n * which here would read as \"there is no primary clone\" on exactly the degraded repo where the\n * answer matters most. It also does not expose the worktree's git NAME, which is the namespace key.\n * • `EffectiveTreeResolver` (#524) answers \"which tree does this COMMAND act on\" — it takes a command\n * string and is a policy input to the bash guards, not a filesystem-path resolver.\n * Both remain authoritative for their own questions. This asks the narrowest one — two path strings —\n * and lives in rules-config, UNDER both, which is where a primitive that pr-gate, ai-hook-rules and\n * code-rules all need has to sit.\n *\n * Fails CLOSED to the pre-change behaviour: when git cannot answer, every path here collapses to\n * `<startDir-root>/.webpieces` exactly as before. Degrading to merely-suboptimal beats throwing on a\n * hook's blocking path.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DotWebpieces {\n // treeRoot → git's answer. One `git rev-parse` pair per root per process; every later path lookup\n // in that invocation is a Map hit.\n private readonly gitDirsByRoot = new Map<string, GitDirs | null>();\n // startDir → `git rev-parse --show-toplevel`, cached on the same terms as gitDirsByRoot.\n private readonly treeRootByDir = new Map<string, string | null>();\n // Roots whose legacy per-worktree `.webpieces/` has already been considered for migration.\n private readonly migrated = new Set<string>();\n\n constructor(private readonly migrator: StateDirMigrator = new StateDirMigrator()) {}\n\n /**\n * REPO-WIDE state: `<primary>/.webpieces`. Use ONLY for facts about the repo rather than about one\n * worktree — today that is merged-branches.json and the main-sync status + lock. Identical from\n * every worktree, and never behind an indirection, so an atomic `rename()` into it is safe.\n */\n shared(startDir: string): string {\n return path.join(this.primaryRoot(startDir), WEBPIECES_TMP_DIR);\n }\n\n /** A path beneath the repo-wide state dir. */\n sharedFile(startDir: string, ...segments: string[]): string {\n return path.join(this.shared(startDir), ...segments);\n }\n\n /**\n * THIS WORKTREE's private state: `<primary>/.webpieces/worktrees/<name>` for a linked worktree, and\n * `<primary>/.webpieces` for the primary clone, which keeps its state exactly where it has always\n * been. Fully isolated — two worktrees never write the same path, so nothing here needs a lock.\n *\n * The first call for a linked worktree also MIGRATES a legacy real `<worktree>/.webpieces/`\n * directory into the namespace, so in-flight merge / pr-review state written under the old scheme\n * (or by an older PUBLISHED build during the transition) is picked up rather than orphaned.\n */\n local(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return this.shared(startDir);\n\n const target = path.join(this.shared(startDir), WORKTREE_STATE_DIR, path.basename(dirs.gitDir));\n this.migrateOnce(startDir, target);\n return target;\n }\n\n /** A path beneath this worktree's private state dir. */\n localFile(startDir: string, ...segments: string[]): string {\n return path.join(this.local(startDir), ...segments);\n }\n\n /**\n * State an AI AGENT writes with its own file-write tool: `<worktree>/.webpieces` — the worktree's\n * OWN root, which is the only place a worktree-isolated agent is permitted to write. Identical to\n * `shared()`/`local()` in the primary clone, so nothing changes for a repo without worktrees.\n *\n * See the \"Why aiWritable() is a third scope\" section above before adding a call site: this is for\n * files the flow ASKS AN AGENT TO WRITE, not for tooling output that an agent merely reads.\n */\n aiWritable(startDir: string): string {\n const toplevel = this.treeRoot(startDir);\n // Fails CLOSED to shared() when git cannot answer, on the same terms as every other path here.\n if (toplevel === null) return this.shared(startDir);\n return path.join(toplevel, WEBPIECES_TMP_DIR);\n }\n\n /** A path beneath the AI-writable state dir. */\n aiWritableFile(startDir: string, ...segments: string[]): string {\n return path.join(this.aiWritable(startDir), ...segments);\n }\n\n /**\n * THE log directory — `<local()>/logs` — and the only place webpieces writes a `.log` or a log's\n * detail files, in the primary clone and in every worktree namespace alike.\n *\n * There is NO migration here, deliberately. A lazy once-per-process relocation of an older\n * release's `hooks/*.log` used to run on this path, and it was itself the defect it claimed to\n * cure: it only fires in the trees a process happens to re-enter, so it guarantees two answers to\n * \"where are the logs\" indefinitely rather than converging on one. New writes go to `logs/` and\n * nothing else; a stale `hooks/` left by an older release is inert and can be deleted by hand.\n */\n logs(startDir: string): string {\n return path.join(this.local(startDir), LOGS_STATE_DIR);\n }\n\n /** A path beneath the log directory — `dotWebpieces.logsFile(root, CALLS_STREAM, writerFile)`. */\n logsFile(startDir: string, ...segments: string[]): string {\n return path.join(this.logs(startDir), ...segments);\n }\n\n /** True when `startDir` sits in a LINKED worktree rather than the primary clone. */\n isLinkedWorktree(startDir: string): boolean {\n const dirs = this.gitDirs(startDir);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * git's two dir answers for `startDir`, memoized — null when it is not a git repo / git is\n * unavailable.\n *\n * PUBLIC because TREE IDENTITY is decided from these two strings and nothing else. `commonDir`\n * is the same for every checkout of ONE repo, so `commonDir(a) === commonDir(b)` is \"same repo,\n * different placement\" — the test that tells a linked worktree (ours, wherever it sits, including\n * inside the repo at `.claude/worktrees/**`) from a nested clone under `repositories/**` (not\n * ours). `gitDirs.isLinkedWorktree` then separates primary from linked. See\n * `EffectiveTreeResolver.classify`, the caller that runs this on the hook's blocking path, which is\n * why it goes through this cache rather than spawning its own `rev-parse`.\n */\n gitDirs(startDir: string): GitDirs | null {\n const cached = this.gitDirsByRoot.get(startDir);\n if (cached !== undefined) return cached;\n\n const gitDir = this.revParse(startDir, '--git-dir');\n const commonDir = this.revParse(startDir, '--git-common-dir');\n const dirs = gitDir === null || commonDir === null ? null : new GitDirs(gitDir, commonDir);\n this.gitDirsByRoot.set(startDir, dirs);\n return dirs;\n }\n\n /**\n * The WORKING TREE root containing `startDir` (`git rev-parse --show-toplevel`), memoized, or null\n * when `startDir` is not in a git repo. In a linked worktree this is the WORKTREE's own root, which\n * is exactly what a caller judging \"which checkout am I standing in\" needs.\n *\n * THE ONE tree-root resolver. There used to be three byte-identical `spawnSync … --show-toplevel`\n * copies — this one, `RepoRootFinder`'s and `effective-tree.ts`'s — each commenting that it\n * \"mirrors\" one of the others, and one of those comments was already stale. Both copies are deleted;\n * everything that needs a tree root calls this. Do not add a fourth.\n */\n treeRoot(startDir: string): string | null {\n const cached = this.treeRootByDir.get(startDir);\n if (cached !== undefined) return cached;\n // `status !== 0` IS the expected \"not a repo\" answer (spawnSync does not throw on a non-zero\n // exit), so nothing here swallows a real git crash.\n const root = this.revParse(startDir, '--show-toplevel');\n this.treeRootByDir.set(startDir, root);\n return root;\n }\n\n /**\n * git's own name for this linked worktree — the basename of `<primary>/.git/worktrees/<name>`, and\n * the namespace key under `worktrees/`. Empty for the primary clone. git's name rather than the\n * directory's basename, so two worktrees checked out into same-named directories under different\n * parents cannot collide.\n */\n worktreeName(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return '';\n return path.basename(dirs.gitDir);\n }\n\n /** The primary clone's root, from any worktree. Falls back to git's toplevel-less best guess. */\n primaryRoot(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null) return startDir;\n if (path.basename(dirs.commonDir) !== GIT_DIR_NAME) return startDir;\n const primary = path.dirname(dirs.commonDir);\n return fs.existsSync(primary) ? primary : startDir;\n }\n\n /**\n * The PRE-change location, `<treeRoot>/.webpieces` — what every call site used to compute. Public\n * because the migrator and its specs must be able to name the thing being migrated FROM, and\n * because the transition-window fallback readers need it.\n */\n legacyDir(treeRoot: string): string {\n return path.join(treeRoot, WEBPIECES_TMP_DIR);\n }\n\n // Drain a legacy per-worktree `.webpieces/` into this worktree's namespace, at most once per tree\n // per process. Migration is idempotent, but it touches the filesystem on the hook's blocking path.\n private migrateOnce(startDir: string, target: string): void {\n // Guard on the CHEAP key first. `local()` is called many times per invocation, and resolving the\n // worktree toplevel costs a `git rev-parse` — doing that before the once-check would put a\n // process spawn on the hook's blocking path for every single state-path lookup.\n if (this.migrated.has(startDir)) return;\n this.migrated.add(startDir);\n const toplevel = this.treeRoot(startDir);\n if (toplevel === null) return;\n this.migrator.migrate(this.legacyDir(toplevel), target, AI_WRITABLE_STATE_DIRS);\n }\n\n // One `git rev-parse <flag>`, resolved to an absolute path (git prints a bare `.git`, relative to\n // the tree, in the primary clone, and an absolute path from a linked worktree).\n private revParse(cwd: string, flag: string): string | null {\n const result = spawnSync('git', ['-C', cwd, 'rev-parse', flag], { encoding: 'utf8' });\n if (result.status !== 0) return null;\n const printed = (result.stdout ?? '').trim();\n if (printed === '') return null;\n return path.resolve(cwd, printed);\n }\n}\n\n// Process-wide instance for the many non-DI call sites (hooks, detached refreshers, wp-* bins, eslint\n// rules). Sharing one instance is what makes the git-resolution cache and the once-per-tree migration\n// worth having; inversify still injects the singleton wherever a container is in play.\nexport const dotWebpieces = new DotWebpieces();\n"]}
|