@webpieces/rules-config 0.4.678 → 0.4.679

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.
@@ -1 +1 @@
1
- {"version":3,"file":"load-template.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/load-template.ts"],"names":[],"mappings":";;;AA6DA,oCAEC;AAED,wDAMC;AAED,sCAMC;;AA/ED,+CAAyB;AACzB,mDAA6B;AAC7B,yCAA2D;AAE3D,+CAA2C;AAC3C,2CAA+C;AAC/C,2CAAyD;AAEzD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;AAC9D,sGAAsG;AACtG,iGAAiG;AACjG,iFAAiF;AACjF,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;GAGG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;IAEF;IACA;IAFrB,YACqB,SAAuB,wBAAY,EACnC,aAAyB,IAAI,wBAAU,EAAE;QADzC,WAAM,GAAN,MAAM,CAA6B;QACnC,eAAU,GAAV,UAAU,CAA+B;IAC3D,CAAC;IAEJ,YAAY,CAAC,IAAY;QACrB,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IACpE,CAAC;IAED,sBAAsB,CAAC,aAAqB,EAAE,IAAY,EAAE,cAAsB,oBAAoB;QAClG,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QAChE,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO;QAChC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED;;;;;;OAMG;IACH,aAAa,CAAC,aAAqB,EAAE,IAAY,EAAE,cAAsB,oBAAoB;QACzF,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QAChE,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,+FAA+F;IAC/F,+CAA+C;IACvC,WAAW,CAAC,aAAqB,EAAE,IAAY,EAAE,WAAmB;QACxE,IAAI,WAAW,KAAK,oBAAoB,EAAE,CAAC;YACvC,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,EAAE,4BAAgB,EAAE,IAAI,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AArCY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGR,wBAAY;QACR,wBAAU;GAHlC,cAAc,CAqC1B;AAED,qGAAqG;AACrG,MAAM,iBAAiB,GAAG,IAAI,cAAc,EAAE,CAAC;AAE/C,SAAgB,YAAY,CAAC,IAAY;IACrC,OAAO,iBAAiB,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,SAAgB,sBAAsB,CAClC,aAAqB,EACrB,IAAY,EACZ,cAAsB,oBAAoB;IAE1C,iBAAiB,CAAC,sBAAsB,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;AAC/E,CAAC;AAED,SAAgB,aAAa,CACzB,aAAqB,EACrB,IAAY,EACZ,cAAsB,oBAAoB;IAE1C,OAAO,iBAAiB,CAAC,aAAa,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { AtomicFile } from './atomic-file';\nimport { INSTRUCT_AI_LEAF } from './repo-root';\nimport { DotWebpieces, dotWebpieces } from './state-dir';\n\nconst TEMPLATES_DIR = path.join(__dirname, '..', 'templates');\n// Sentinel for \"use the resolved LOCAL instruct-ai dir\". Kept as the parameter default so the handful\n// of callers that pass an explicit relative dir (they join it onto workspaceRoot themselves) are\n// unaffected; anything passing the default gets DotWebpieces.local() resolution.\nconst DEFAULT_INSTRUCT_DIR = '';\n\n/**\n * Writes the AI-facing instruct-ai template docs under `<workspaceRoot>/.webpieces/instruct-ai/`.\n * `@injectable(bindingScopeValues.Singleton)` so it can be injected and appear in the rules-config DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class TemplateWriter {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n ) {}\n\n loadTemplate(name: string): string {\n return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf-8');\n }\n\n writeTemplateIfMissing(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): void {\n const dest = this.destination(workspaceRoot, name, instructDir);\n if (fs.existsSync(dest)) return;\n this.atomicFile.writeAtomic(dest, this.loadTemplate(name));\n }\n\n /**\n * Rewrite the doc, ATOMICALLY and only when its bytes actually changed.\n *\n * Every `wp-*` command regenerates these, and the AI is routinely told to open one by absolute\n * path. A plain truncating write means a reader can catch it empty; skip-if-unchanged means the\n * overwhelmingly common case (same package version ⇒ identical content) does not write at all.\n */\n writeTemplate(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): string {\n const dest = this.destination(workspaceRoot, name, instructDir);\n this.atomicFile.writeIfChanged(dest, this.loadTemplate(name));\n return dest;\n }\n\n // LOCAL `.webpieces/instruct-ai/<name>` by default; an explicitly-passed relative dir is still\n // joined onto workspaceRoot exactly as before.\n private destination(workspaceRoot: string, name: string, instructDir: string): string {\n if (instructDir === DEFAULT_INSTRUCT_DIR) {\n return this.dotDir.localFile(workspaceRoot, INSTRUCT_AI_LEAF, name);\n }\n return path.join(workspaceRoot, instructDir, name);\n }\n}\n\n// Temporary migration delegators — consumers migrate to injecting TemplateWriter over follow-up PRs.\nconst templateWriterSvc = new TemplateWriter();\n\nexport function loadTemplate(name: string): string {\n return templateWriterSvc.loadTemplate(name);\n}\n\nexport function writeTemplateIfMissing(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): void {\n templateWriterSvc.writeTemplateIfMissing(workspaceRoot, name, instructDir);\n}\n\nexport function writeTemplate(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): string {\n return templateWriterSvc.writeTemplate(workspaceRoot, name, instructDir);\n}\n"]}
1
+ {"version":3,"file":"load-template.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/load-template.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 { AtomicFile } from './atomic-file';\nimport { InstructAiDocSet } from './instruct-ai-docs';\nimport { INSTRUCT_AI_LEAF } from './repo-root';\nimport { DotWebpieces, dotWebpieces } from './state-dir';\n\nconst TEMPLATES_DIR = path.join(__dirname, '..', 'templates');\n// Sentinel for \"use the resolved LOCAL instruct-ai dir\". Kept as the parameter default so the handful\n// of callers that pass an explicit relative dir (they join it onto workspaceRoot themselves) are\n// unaffected; anything passing the default gets DotWebpieces.local() resolution.\nconst DEFAULT_INSTRUCT_DIR = '';\n\n/**\n * Writes the AI-facing instruct-ai template docs under `<workspaceRoot>/.webpieces/instruct-ai/`.\n * `@injectable(bindingScopeValues.Singleton)` so it can be injected and appear in the rules-config DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class TemplateWriter {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n private readonly docs: InstructAiDocSet = new InstructAiDocSet(),\n ) {}\n\n loadTemplate(name: string): string {\n return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf-8');\n }\n\n /**\n * SEED `name` and everything it links to — writing only the ones that are not already on disk.\n *\n * Same closure as `writeTemplate`, for the same reason: a rule that drops\n * `webpieces.exceptions.md` next to its violation is delivering a doc a reader follows links out\n * of, and a seeded doc that later gains a sibling link would otherwise dangle exactly the way\n * git-workflow.md's link to the merge process did. The difference from `writeTemplate` is only\n * WHETHER an existing file is refreshed, never WHICH files are considered.\n */\n writeTemplateIfMissing(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): void {\n for (const doc of this.docs.closure(name, (docName: string): string => this.loadTemplate(docName))) {\n const target = this.destination(workspaceRoot, doc.name, instructDir);\n if (fs.existsSync(target)) continue;\n this.atomicFile.writeAtomic(target, doc.render(this.loadTemplate(doc.name)));\n }\n }\n\n /**\n * Write `name` AND every instruct-ai doc it links to, ATOMICALLY and only where bytes changed.\n * Returns the absolute path of `name` itself.\n *\n * THE CLOSURE IS THE POINT. Callers name the ONE doc their command is about; the docs a reader is\n * sent on to arrive with it, because a doc whose links dangle is worse than no doc — it teaches the\n * reader that the paths in these files cannot be trusted. See instruct-ai-docs.ts for the incident.\n *\n * Every `wp-*` command regenerates these, and the AI is routinely told to open one by absolute\n * path. A plain truncating write means a reader can catch it empty; skip-if-unchanged means the\n * overwhelmingly common case (same package version ⇒ identical content) does not write at all.\n */\n writeTemplate(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): string {\n for (const doc of this.docs.closure(name, (docName: string): string => this.loadTemplate(docName))) {\n const target = this.destination(workspaceRoot, doc.name, instructDir);\n // A doc stamped with live run state is SEEDED, never refreshed: `wp-finish-upsert-pr` runs\n // while a conflicted merge is still open, and clobbering that handback with the reference\n // copy would delete the file list the agent is working from.\n if (doc.seedOnly() && fs.existsSync(target)) continue;\n this.atomicFile.writeIfChanged(target, doc.render(this.loadTemplate(doc.name)));\n }\n return this.destination(workspaceRoot, name, instructDir);\n }\n\n // LOCAL `.webpieces/instruct-ai/<name>` by default; an explicitly-passed relative dir is still\n // joined onto workspaceRoot exactly as before.\n private destination(workspaceRoot: string, name: string, instructDir: string): string {\n if (instructDir === DEFAULT_INSTRUCT_DIR) {\n return this.dotDir.localFile(workspaceRoot, INSTRUCT_AI_LEAF, name);\n }\n return path.join(workspaceRoot, instructDir, name);\n }\n}\n\n// Temporary migration delegators — consumers migrate to injecting TemplateWriter over follow-up PRs.\nconst templateWriterSvc = new TemplateWriter();\n\nexport function loadTemplate(name: string): string {\n return templateWriterSvc.loadTemplate(name);\n}\n\nexport function writeTemplateIfMissing(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): void {\n templateWriterSvc.writeTemplateIfMissing(workspaceRoot, name, instructDir);\n}\n\nexport function writeTemplate(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): string {\n return templateWriterSvc.writeTemplate(workspaceRoot, name, instructDir);\n}\n"]}
@@ -0,0 +1,47 @@
1
+ /** The rules-config template name, and the file name it lands under in `.webpieces/instruct-ai/`. */
2
+ export declare const MERGE_PROCESS_DOC = "webpieces.mergeprocess.md";
3
+ /**
4
+ * The values one merge run stamps into the doc, plus the one rendering that depends on WHICH state
5
+ * this is.
6
+ *
7
+ * `ReferenceMergeRun` below is not a second document — it is these same fields standing for "no run in
8
+ * progress", which is exactly what a reader of the delivered copy has to be told before following the
9
+ * steps. One class and its subclass, so there is one merge process and two things it can be about.
10
+ */
11
+ export declare class MergeRun {
12
+ /** The `wp-start-*` command that produced this state. */
13
+ readonly startCommand: string;
14
+ /** Its paired `wp-finish-*` command — the ONLY way out of a conflicted merge. */
15
+ readonly finishCommand: string;
16
+ /** The transient `<feature>Squash` branch the merge put the caller on. */
17
+ readonly squashBranch: string;
18
+ /** Absolute path of this run's `merge-<n>/` context dir. */
19
+ readonly mergeDir: string;
20
+ /** Markdown bullet list of the conflicted files. */
21
+ readonly fileList: string;
22
+ constructor(
23
+ /** The `wp-start-*` command that produced this state. */
24
+ startCommand: string,
25
+ /** Its paired `wp-finish-*` command — the ONLY way out of a conflicted merge. */
26
+ finishCommand: string,
27
+ /** The transient `<feature>Squash` branch the merge put the caller on. */
28
+ squashBranch: string,
29
+ /** Absolute path of this run's `merge-<n>/` context dir. */
30
+ mergeDir: string,
31
+ /** Markdown bullet list of the conflicted files. */
32
+ fileList: string);
33
+ /** The doc's opening block: a LIVE conflict handback naming this run's branch and dir. */
34
+ runState(): string;
35
+ }
36
+ /** The state of a tree with NO conflicted merge in it — what the delivered reference copy describes. */
37
+ export declare class ReferenceMergeRun extends MergeRun {
38
+ constructor();
39
+ runState(): string;
40
+ }
41
+ /** Substitutes one `MergeRun` (and the fixed explanation-file name) into the merge-process template. */
42
+ export declare class MergeProcessText {
43
+ private readonly template;
44
+ private readonly explanationFile;
45
+ constructor(template: string, explanationFile?: string);
46
+ render(run: MergeRun): string;
47
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ // ---------------------------------------------------------------------------
3
+ // `webpieces.mergeprocess.md` — ONE template, TWO inputs.
4
+ //
5
+ // The merge-process doc used to live as a template literal inside pr-gate's `merge-start.ts`, and was
6
+ // written ONLY when a 3-point merge actually conflicted. `webpieces.git-workflow.md` links to it
7
+ // unconditionally, so in every tree that had not yet hit a conflict that link pointed at nothing —
8
+ // measured in six of nine governed repos.
9
+ //
10
+ // The cure is not "write it from one more place". It is to make the doc a rules-config TEMPLATE like
11
+ // every other instruct-ai doc, so `InstructAiDocSet` delivers it as part of git-workflow.md's LINK
12
+ // CLOSURE, and to give that template one renderer with two possible inputs:
13
+ //
14
+ // new ReferenceMergeRun() the tree has no conflicted merge — the doc reads as the PROCESS
15
+ // new MergeRun(...) pr-gate's conflict handback — the doc reads as a LIVE handback
16
+ //
17
+ // Two inputs, one output shape: there is exactly one spelling of the merge process, and the delivered
18
+ // copy cannot drift from it because the delivered copy IS it.
19
+ // ---------------------------------------------------------------------------
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.MergeProcessText = exports.ReferenceMergeRun = exports.MergeRun = exports.MERGE_PROCESS_DOC = void 0;
22
+ const constants_1 = require("./constants");
23
+ /** The rules-config template name, and the file name it lands under in `.webpieces/instruct-ai/`. */
24
+ exports.MERGE_PROCESS_DOC = 'webpieces.mergeprocess.md';
25
+ const REFERENCE_RUN_STATE = `**Reference copy — no conflicted 3-point merge is in progress in this tree.** It is written beside
26
+ \`webpieces.git-workflow.md\` on every \`wp-*\` command, so the link from that doc always resolves.
27
+
28
+ When a 3-point merge DOES conflict, \`pnpm wp-start-upsert-pr\` (or \`pnpm wp-start-update\`) rewrites
29
+ this file in place with the branch you are standing on, that run's absolute \`MERGE_DIR\`, the exact
30
+ finish command for the flow you started, and the list of conflicted files. Until that happens, read
31
+ what follows as the PROCESS and not as a live handback: \`<feature>Squash\` stands for the transient
32
+ branch a conflicted merge puts you on, and \`MERGE_DIR\` for that run's
33
+ \`.webpieces/merge-info/<feature>/merge-<n>/\` directory.`;
34
+ const REFERENCE_FILE_LIST = '*(none — no conflicted merge is in progress; the live copy lists this run\'s conflicted files here)*';
35
+ /**
36
+ * The values one merge run stamps into the doc, plus the one rendering that depends on WHICH state
37
+ * this is.
38
+ *
39
+ * `ReferenceMergeRun` below is not a second document — it is these same fields standing for "no run in
40
+ * progress", which is exactly what a reader of the delivered copy has to be told before following the
41
+ * steps. One class and its subclass, so there is one merge process and two things it can be about.
42
+ */
43
+ class MergeRun {
44
+ startCommand;
45
+ finishCommand;
46
+ squashBranch;
47
+ mergeDir;
48
+ fileList;
49
+ // eslint-disable-next-line @typescript-eslint/max-params -- the five values one merge run stamps in
50
+ constructor(
51
+ /** The `wp-start-*` command that produced this state. */
52
+ startCommand,
53
+ /** Its paired `wp-finish-*` command — the ONLY way out of a conflicted merge. */
54
+ finishCommand,
55
+ /** The transient `<feature>Squash` branch the merge put the caller on. */
56
+ squashBranch,
57
+ /** Absolute path of this run's `merge-<n>/` context dir. */
58
+ mergeDir,
59
+ /** Markdown bullet list of the conflicted files. */
60
+ fileList) {
61
+ this.startCommand = startCommand;
62
+ this.finishCommand = finishCommand;
63
+ this.squashBranch = squashBranch;
64
+ this.mergeDir = mergeDir;
65
+ this.fileList = fileList;
66
+ }
67
+ /** The doc's opening block: a LIVE conflict handback naming this run's branch and dir. */
68
+ runState() {
69
+ return `This file was generated by \`pnpm ${this.startCommand}\` when the 3-point squash-merge hit conflicts.\n` +
70
+ 'It is the single source of truth for the merge process — follow it exactly.\n\n' +
71
+ `You are on branch \`${this.squashBranch}\` with conflict markers in the working tree.\n` +
72
+ `\`MERGE_DIR = ${this.mergeDir}\``;
73
+ }
74
+ }
75
+ exports.MergeRun = MergeRun;
76
+ /** The state of a tree with NO conflicted merge in it — what the delivered reference copy describes. */
77
+ class ReferenceMergeRun extends MergeRun {
78
+ constructor() {
79
+ super('wp-start-upsert-pr', 'wp-finish-upsert-pr', '<feature>Squash', 'MERGE_DIR', REFERENCE_FILE_LIST);
80
+ }
81
+ runState() {
82
+ return REFERENCE_RUN_STATE;
83
+ }
84
+ }
85
+ exports.ReferenceMergeRun = ReferenceMergeRun;
86
+ /** Substitutes one `MergeRun` (and the fixed explanation-file name) into the merge-process template. */
87
+ class MergeProcessText {
88
+ template;
89
+ explanationFile;
90
+ constructor(template, explanationFile = constants_1.MERGE_EXPLANATION_FILE) {
91
+ this.template = template;
92
+ this.explanationFile = explanationFile;
93
+ }
94
+ render(run) {
95
+ return this.template
96
+ .replace(/\{\{RUN_STATE\}\}/g, run.runState())
97
+ .replace(/\{\{SQUASH_BRANCH\}\}/g, run.squashBranch)
98
+ .replace(/\{\{MERGE_DIR\}\}/g, run.mergeDir)
99
+ .replace(/\{\{EXPLANATION_FILE\}\}/g, this.explanationFile)
100
+ .replace(/\{\{FINISH_COMMAND\}\}/g, run.finishCommand)
101
+ .replace(/\{\{START_COMMAND\}\}/g, run.startCommand)
102
+ .replace(/\{\{FILE_LIST\}\}/g, run.fileList);
103
+ }
104
+ }
105
+ exports.MergeProcessText = MergeProcessText;
106
+ //# sourceMappingURL=merge-process-doc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-process-doc.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/merge-process-doc.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,0DAA0D;AAC1D,EAAE;AACF,sGAAsG;AACtG,iGAAiG;AACjG,mGAAmG;AACnG,0CAA0C;AAC1C,EAAE;AACF,qGAAqG;AACrG,mGAAmG;AACnG,4EAA4E;AAC5E,EAAE;AACF,6FAA6F;AAC7F,4FAA4F;AAC5F,EAAE;AACF,sGAAsG;AACtG,8DAA8D;AAC9D,8EAA8E;;;AAE9E,2CAAqD;AAErD,qGAAqG;AACxF,QAAA,iBAAiB,GAAG,2BAA2B,CAAC;AAE7D,MAAM,mBAAmB,GAAG;;;;;;;;0DAQ8B,CAAC;AAE3D,MAAM,mBAAmB,GACrB,sGAAsG,CAAC;AAE3G;;;;;;;GAOG;AACH,MAAa,QAAQ;IAIJ;IAEA;IAEA;IAEA;IAEA;IAXb,oGAAoG;IACpG;IACI,yDAAyD;IAChD,YAAoB;IAC7B,iFAAiF;IACxE,aAAqB;IAC9B,0EAA0E;IACjE,YAAoB;IAC7B,4DAA4D;IACnD,QAAgB;IACzB,oDAAoD;IAC3C,QAAgB;QARhB,iBAAY,GAAZ,YAAY,CAAQ;QAEpB,kBAAa,GAAb,aAAa,CAAQ;QAErB,iBAAY,GAAZ,YAAY,CAAQ;QAEpB,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,aAAQ,GAAR,QAAQ,CAAQ;IAC1B,CAAC;IAEJ,0FAA0F;IAC1F,QAAQ;QACJ,OAAO,qCAAqC,IAAI,CAAC,YAAY,mDAAmD;YAC5G,iFAAiF;YACjF,uBAAuB,IAAI,CAAC,YAAY,iDAAiD;YACzF,iBAAiB,IAAI,CAAC,QAAQ,IAAI,CAAC;IAC3C,CAAC;CACJ;AAtBD,4BAsBC;AAED,wGAAwG;AACxG,MAAa,iBAAkB,SAAQ,QAAQ;IAC3C;QACI,KAAK,CAAC,oBAAoB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,WAAW,EAAE,mBAAmB,CAAC,CAAC;IAC5G,CAAC;IAEQ,QAAQ;QACb,OAAO,mBAAmB,CAAC;IAC/B,CAAC;CACJ;AARD,8CAQC;AAED,wGAAwG;AACxG,MAAa,gBAAgB;IACI;IAAmC;IAAhE,YAA6B,QAAgB,EAAmB,kBAA0B,kCAAsB;QAAnF,aAAQ,GAAR,QAAQ,CAAQ;QAAmB,oBAAe,GAAf,eAAe,CAAiC;IAAG,CAAC;IAEpH,MAAM,CAAC,GAAa;QAChB,OAAO,IAAI,CAAC,QAAQ;aACf,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;aAC7C,OAAO,CAAC,wBAAwB,EAAE,GAAG,CAAC,YAAY,CAAC;aACnD,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC,QAAQ,CAAC;aAC3C,OAAO,CAAC,2BAA2B,EAAE,IAAI,CAAC,eAAe,CAAC;aAC1D,OAAO,CAAC,yBAAyB,EAAE,GAAG,CAAC,aAAa,CAAC;aACrD,OAAO,CAAC,wBAAwB,EAAE,GAAG,CAAC,YAAY,CAAC;aACnD,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;CACJ;AAbD,4CAaC","sourcesContent":["// ---------------------------------------------------------------------------\n// `webpieces.mergeprocess.md` — ONE template, TWO inputs.\n//\n// The merge-process doc used to live as a template literal inside pr-gate's `merge-start.ts`, and was\n// written ONLY when a 3-point merge actually conflicted. `webpieces.git-workflow.md` links to it\n// unconditionally, so in every tree that had not yet hit a conflict that link pointed at nothing —\n// measured in six of nine governed repos.\n//\n// The cure is not \"write it from one more place\". It is to make the doc a rules-config TEMPLATE like\n// every other instruct-ai doc, so `InstructAiDocSet` delivers it as part of git-workflow.md's LINK\n// CLOSURE, and to give that template one renderer with two possible inputs:\n//\n// new ReferenceMergeRun() the tree has no conflicted merge — the doc reads as the PROCESS\n// new MergeRun(...) pr-gate's conflict handback — the doc reads as a LIVE handback\n//\n// Two inputs, one output shape: there is exactly one spelling of the merge process, and the delivered\n// copy cannot drift from it because the delivered copy IS it.\n// ---------------------------------------------------------------------------\n\nimport { MERGE_EXPLANATION_FILE } from './constants';\n\n/** The rules-config template name, and the file name it lands under in `.webpieces/instruct-ai/`. */\nexport const MERGE_PROCESS_DOC = 'webpieces.mergeprocess.md';\n\nconst REFERENCE_RUN_STATE = `**Reference copy — no conflicted 3-point merge is in progress in this tree.** It is written beside\n\\`webpieces.git-workflow.md\\` on every \\`wp-*\\` command, so the link from that doc always resolves.\n\nWhen a 3-point merge DOES conflict, \\`pnpm wp-start-upsert-pr\\` (or \\`pnpm wp-start-update\\`) rewrites\nthis file in place with the branch you are standing on, that run's absolute \\`MERGE_DIR\\`, the exact\nfinish command for the flow you started, and the list of conflicted files. Until that happens, read\nwhat follows as the PROCESS and not as a live handback: \\`<feature>Squash\\` stands for the transient\nbranch a conflicted merge puts you on, and \\`MERGE_DIR\\` for that run's\n\\`.webpieces/merge-info/<feature>/merge-<n>/\\` directory.`;\n\nconst REFERENCE_FILE_LIST =\n '*(none — no conflicted merge is in progress; the live copy lists this run\\'s conflicted files here)*';\n\n/**\n * The values one merge run stamps into the doc, plus the one rendering that depends on WHICH state\n * this is.\n *\n * `ReferenceMergeRun` below is not a second document — it is these same fields standing for \"no run in\n * progress\", which is exactly what a reader of the delivered copy has to be told before following the\n * steps. One class and its subclass, so there is one merge process and two things it can be about.\n */\nexport class MergeRun {\n // eslint-disable-next-line @typescript-eslint/max-params -- the five values one merge run stamps in\n constructor(\n /** The `wp-start-*` command that produced this state. */\n readonly startCommand: string,\n /** Its paired `wp-finish-*` command — the ONLY way out of a conflicted merge. */\n readonly finishCommand: string,\n /** The transient `<feature>Squash` branch the merge put the caller on. */\n readonly squashBranch: string,\n /** Absolute path of this run's `merge-<n>/` context dir. */\n readonly mergeDir: string,\n /** Markdown bullet list of the conflicted files. */\n readonly fileList: string,\n ) {}\n\n /** The doc's opening block: a LIVE conflict handback naming this run's branch and dir. */\n runState(): string {\n return `This file was generated by \\`pnpm ${this.startCommand}\\` when the 3-point squash-merge hit conflicts.\\n` +\n 'It is the single source of truth for the merge process — follow it exactly.\\n\\n' +\n `You are on branch \\`${this.squashBranch}\\` with conflict markers in the working tree.\\n` +\n `\\`MERGE_DIR = ${this.mergeDir}\\``;\n }\n}\n\n/** The state of a tree with NO conflicted merge in it — what the delivered reference copy describes. */\nexport class ReferenceMergeRun extends MergeRun {\n constructor() {\n super('wp-start-upsert-pr', 'wp-finish-upsert-pr', '<feature>Squash', 'MERGE_DIR', REFERENCE_FILE_LIST);\n }\n\n override runState(): string {\n return REFERENCE_RUN_STATE;\n }\n}\n\n/** Substitutes one `MergeRun` (and the fixed explanation-file name) into the merge-process template. */\nexport class MergeProcessText {\n constructor(private readonly template: string, private readonly explanationFile: string = MERGE_EXPLANATION_FILE) {}\n\n render(run: MergeRun): string {\n return this.template\n .replace(/\\{\\{RUN_STATE\\}\\}/g, run.runState())\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, run.squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, run.mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, this.explanationFile)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, run.finishCommand)\n .replace(/\\{\\{START_COMMAND\\}\\}/g, run.startCommand)\n .replace(/\\{\\{FILE_LIST\\}\\}/g, run.fileList);\n }\n}\n"]}
@@ -0,0 +1,70 @@
1
+ # Where the build's output is — read the printed path, never a remembered one
2
+
3
+ `pnpm wp-build` and the PR gate's build stages send the build's **entire** stdout and stderr to a FILE.
4
+ Your terminal gets a heartbeat and a summary; it never gets the build output. So every question about a
5
+ build ("what failed", "did the tests run", "what was that warning") is answered by reading a file — never
6
+ by running the build again.
7
+
8
+ ## THE RULE
9
+
10
+ **Read the log at the absolute path the `FullLog :` line of THIS run printed.**
11
+
12
+ Every run — success or failure — ends with two lines:
13
+
14
+ ```
15
+ FullLog : /abs/path/to/build.log
16
+ (a second line, naming that log's `.bak` — the previous run is rotated aside on every build)
17
+ ```
18
+
19
+ Copy that path. `grep -n error "<the FullLog path>"` always works. A path typed from memory does not:
20
+ it either greps a file from some other tree or greps nothing at all and reports "no matches", which
21
+ reads exactly like a clean build.
22
+
23
+ ## WHY A REMEMBERED PATH IS WRONG MORE OFTEN THAN IT IS RIGHT
24
+
25
+ webpieces state is worktree-namespaced **inside the primary clone**, not duplicated per worktree:
26
+
27
+ | where you are standing | where `wp-build` writes |
28
+ |---|---|
29
+ | the primary clone | `<primary>/.webpieces/build.log` |
30
+ | a linked worktree (the normal case for a subagent) | `<primary>/.webpieces/worktrees/<worktree-name>/build.log` |
31
+
32
+ A relative `.webpieces/build.log` inside a linked worktree **does not exist at all**. Those two rows are
33
+ an illustration of why you read the printed path — they are not a lookup table to memorise, and the
34
+ namespacing scheme is webpieces' to change.
35
+
36
+ The PR gate's own build stages use a different name again — `logs/build-gate-<stage>-<branch>-<sha>.log`
37
+ — precisely because two stages can build the same commit and neither may overwrite the other's evidence.
38
+ Nobody types those names either. The failure message prints them.
39
+
40
+ ## WHAT THE CONSOLE GIVES YOU WHILE IT RUNS
41
+
42
+ A heartbeat every few seconds: `<log> size <n> lines`, with the word `still` appended when the count has
43
+ not moved since the last tick. `still` is the load-bearing word — a build that is linking, or waiting on
44
+ a cold nx cache, produces no output for minutes, and without it you cannot tell a stalled BUILD from a
45
+ stalled reporter. Neither one is a reason to start a second build.
46
+
47
+ ## ON A RED BUILD
48
+
49
+ The failure summary already echoes the last lines of the log, so the immediate cause usually needs no
50
+ second command. For anything more, `grep` the FILE.
51
+
52
+ **Never re-run the build to see a different slice of its output.** That is the whole reason the file
53
+ exists: one measured session spent 23.9 minutes across nine builds, five of them with no code change in
54
+ between, walking `| tail -50` → `> /tmp/file` → `| grep` → `| sed -n '1100,1230p'` over output that had
55
+ already scrolled past. Every one of those is a `grep` of the `FullLog :` file now — and the run before it
56
+ is still on disk as `.bak`.
57
+
58
+ If you read that file and find no failure in it, something upstream is wrong (a runner that died without
59
+ printing, a truncated redirect). Report that contradiction to the human and stop. Do not guess, and do
60
+ not rebuild.
61
+
62
+ ## WHAT TO RUN, AND WHAT NOT TO
63
+
64
+ One command: **`pnpm wp-build`**. It runs `commands.pr-gate.buildCommand` from `webpieces.config.json`
65
+ verbatim, through the same resolver the PR gate's build stage uses, and prints the command it resolved
66
+ before running it — so a green result locally is evidence about the gate.
67
+
68
+ Do not hand-compose a verify chain of your own, and do not add a leg to `wp-build`. Anything that must
69
+ run on every build belongs *inside* `buildCommand`, where the gate runs it too. For the inner loop, run
70
+ the one spec file you are changing.
@@ -178,7 +178,8 @@ flow are not interchangeable.
178
178
  - `wp-start-update` **pairs with** `wp-finish-update`. `wp-start-upsert-pr` **pairs with**
179
179
  `wp-review-upsert-pr` then `wp-finish-upsert-pr` — in the PR flow the middle command is not optional,
180
180
  and it is what validates a conflict resolution. A start from one pair plus a finish from the other is not a flow — if you are
181
- unsure which run you are in, the generated `webpieces.mergeprocess.md` names your exact finish command.
181
+ unsure which run you are in, read `webpieces.mergeprocess.md`: once a merge has conflicted its opening
182
+ block is a live handback and names your exact finish command (before that it says it is a reference copy).
182
183
  - **An open PR MUST use flow B.** The 3-point merge rewrites this branch (squashes onto main and
183
184
  force-pushes a new generation), so the open PR's history is blown away and has to be re-pointed in the
184
185
  same run. Flow A never touches the PR, so running it with a PR open would strand that PR on the OLD
@@ -445,9 +446,28 @@ Who does what on the conflict path:
445
446
  validation). `git add -u` stages only your tracked resolutions and **refuses** if any
446
447
  untracked file is present — commit or delete those first, the tool will not sweep them in.
447
448
 
448
- The two docs cooperate: **this file** is the always-present overview of both paths;
449
- **`webpieces.mergeprocess.md`** is written *only when there is a conflict* and lists the exact files
450
- to resolve for that specific merge.
449
+ The two docs cooperate: **this file** is the overview of both paths; **`webpieces.mergeprocess.md`** is
450
+ the merge process itself. It is written beside this one on every `wp-*` command, so the link always
451
+ resolves as a reference copy while nothing is conflicted, and rewritten in place by `wp-start-*` with
452
+ this run's branch, `MERGE_DIR`, finish command and conflicted-file list the moment a merge does conflict.
453
+ Its opening lines say which of the two you are looking at.
454
+
455
+ ## Sibling docs in this directory
456
+
457
+ Every doc named here is written into `.webpieces/instruct-ai/` together with this one — a doc and
458
+ everything it links to always land as a set, so a link from this page never points at a file that is not
459
+ there.
460
+
461
+ | doc | read it when |
462
+ |---|---|
463
+ | `webpieces.mergeprocess.md` | a 3-point merge conflicted, or you want the resolution procedure |
464
+ | `webpieces.buildlog.md` | you need the build's output — **the log is a FILE, and the path is the one `FullLog :` printed this run** |
465
+ | `webpieces.location-matrix.md` | a guard decision or log line says `layer=L1 row=<n>` — L1 is location: jurisdiction, version skew, and git-from-the-root |
466
+ | `webpieces.branch-state-matrix.md` | it says `layer=L2 row=<n>` — L2 is branch state: may I work here, is what I read current |
467
+ | `webpieces.guard-matrix.md` | it says `layer=L0` or names a fault code — L0 is whether webpieces itself is trustworthy right now |
468
+
469
+ The three matrices are generated from the very arrays the guards dispatch on, so a row on the page is
470
+ the row that judged your call — not a description of it.
451
471
 
452
472
  ## Before you trust a push
453
473
 
@@ -0,0 +1,247 @@
1
+ # L1 — location
2
+
3
+ **Goal: is this call ours to judge, is this tree governed by the release it asks for, and is git being run from the root?**
4
+
5
+ **Config key: none, and none is proposed.** Force-to-root and trinary-version-skew have **no config
6
+ key** and cannot be disabled; `excludePaths` is a top-level block, not a `hookGuards` entry. A
7
+ `location-guard` key was once proposed here — it never existed, and `hookGuards` has just gone from
8
+ nine keys to three, so adding a tenth-turned-fourth for a layer nobody has asked to switch off would
9
+ run against the whole point. L0 has no key for the stronger version of the same reason: a layer that
10
+ decides whether the tooling can be trusted cannot be configured by the file it has not validated yet.
11
+
12
+
13
+ **Code:** `packages/tooling/ai-hook-rules/src/core/effective-tree.ts` (`EffectiveTreeResolver`,
14
+ `TreeKind`) · `packages/tooling/ai-hook-rules/src/core/runner.ts` (`l1LocationBlock`,
15
+ `filterByExcludedPaths`, the `foreign` check) · `.../force-to-root.ts` (`ForceToRootGuard`) ·
16
+ `packages/tooling/ai-hook-rules/src/core/missing-directory.ts` (`MissingDirectoryGuard`) ·
17
+ `packages/tooling/ai-hook-rules/src/core/version-sync.ts` (`VersionSyncGuard`,
18
+ `WebpiecesVersions`).
19
+
20
+ L1 answers four questions, and they are genuinely separate:
21
+
22
+ 1. **Do we govern this at all?** — the escape hatches, for other repos and non-governed paths.
23
+ Answered by asking GIT (`--git-common-dir`), never by path math: see the legend under **K**.
24
+ 2. **Does the directory still EXIST?** — row 7. A worktree reaped out from under a live shell leaves
25
+ a cwd that names nothing, and that state needs its own name and its own message, because the
26
+ remedy for "you are in a subdirectory" is a `cd` back into the very directory that is gone.
27
+ 3. **Is this tree governed by a release it did not ask for?** — row 8. A worktree MAY have its own
28
+ `node_modules` (nx, vitest and the eslint plugin all execute there and load from it), and when it
29
+ has none the shim's upward walk runs the main tree's binary. Either way the rule is the same and
30
+ holds whichever registration form — absolute or relative — is live in the consumer: the two trees
31
+ must PIN the same `@webpieces`, or the worktree is linted, validated and built by a release its
32
+ own manifest does not ask for. Asked of the PATH acted on, never of who is asking —
33
+ agent identity was measured untrustworthy for tree detection (a worktree-isolated agent whose tree
34
+ is auto-reaped at a turn boundary silently resumes on the primary clone).
35
+ 4. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where
36
+ they are constantly, and `cd` gives them two different ways to be wrong: a `cd` that stays INSIDE
37
+ the workspace PERSISTS to later calls (so the shell can be parked in a subdirectory left by an
38
+ unrelated command turns earlier), while a `cd` that LEAVES it is reset by the harness, which says
39
+ so — `Shell cwd was reset to <root>`. Neither can be assumed, which is why every remedy names the
40
+ root explicitly instead of telling the agent to `cd` first.
41
+
42
+ ## Preamble — resolve the target first (Bash only)
43
+
44
+ `EffectiveTreeResolver.resolve()` computes `effectiveCwd`: the directory the command actually runs in,
45
+ which is the shell's cwd unless the command leads with `cd <dir> &&`. **K is classified from
46
+ `effectiveCwd`, not from the shell's cwd** — so "a foreign repo that `cd`s into ours" is not a cell,
47
+ it is simply `pw` after resolution.
48
+
49
+ That holds because K is resolved by ASKING GIT about `effectiveCwd`, not by testing whether the path
50
+ is lexically under the governed root. It has to be said that way round: the resolver used to do the
51
+ path test, and a linked worktree under `.claude/worktrees/**` therefore resolved `f` — every bash
52
+ guard exempt in the one sandbox agents are told to work in. A sentence in this file asserted the
53
+ opposite as fact for several releases, which is how it went unnoticed.
54
+
55
+ Only a LEADING run of `cd`/`pushd` counts. A *trailing* `… && cd <exempt-tree>` must never
56
+ retroactively pull a command out of scope — that would smuggle a root-level `git push` past the
57
+ guards. Quoting is handled by `ShellSegmentScan`, so `echo "cd sub && git push"` is one opaque
58
+ segment and its quoted `cd` is never picked up.
59
+
60
+ ## Filter — not a dimension (all tools)
61
+
62
+ `filterByExcludedPaths` drops every rule excluded for this path: the **target path** for
63
+ Read/Write/Edit, `effectiveCwd` for Bash. An empty rule list means allow. This is a filter, not a row:
64
+ "exempt" is what emerges when the list empties.
65
+
66
+ ONE path is filtered out BEFORE the list is consulted and cannot be put back: **`.webpieces/`**,
67
+ the tooling's own state dir (`isWebpiecesStateDir`). It is gitignored in every consumer, so nothing
68
+ under it can reach a branch, be reviewed or be reverted — every reason L2 prints for protecting
69
+ `main` is vacuous there. It was config-only once, which made the exemption optional on exactly the
70
+ directory webpieces itself writes to: `wp-review-upsert-pr` hands a reviewer subagent a
71
+ `<primary>/.webpieces/worktrees/agent-<id>/pr-review/…` path, that write resolves to the PRIMARY
72
+ clone, and L2 judged the primary's live branch — so the reviewer was denied "You should not be
73
+ working on main" whenever an unrelated session had left the primary there. There is deliberately
74
+ NO companion `".webpieces/**"` glob seeded into `excludePaths`: a config entry that changes
75
+ nothing is a second and WEAKER spelling — the matcher below misses the bare directory that the
76
+ predicate matches — and it invites a consumer to delete it and believe the exemption went too.
77
+
78
+ `excludePaths` is **ONE glob list** (canonical: `"excludePaths": ["repositories/**"]`). The
79
+ `{ rules: [...], guards: [...] }` object is **retired and rejected**, with the union it must become
80
+ named in the error. `wp-install-ai-hooks` migrates it in place.
81
+
82
+ This used to be a tolerated fallback, justified here by "rejecting it would block every Bash/Edit
83
+ including the edit that would fix it." **That was never true**, and the fallback it licensed is why
84
+ consumer configs — this repo's own included — sat on the dead shape for releases. A Write/Edit whose
85
+ target is `webpieces.config.json` is an unconditional **PASS** (see the L0 table above), and
86
+ `pnpm install` has an installer bypass, so an invalid config can always be repaired from inside the
87
+ block. Config rejection is self-recoverable by construction; see `retired-config-keys.ts` for the
88
+ policy and the reasoning.
89
+
90
+ ## Legend
91
+
92
+ | col | dimension | values |
93
+ |---|---|---|
94
+ | **K** | tree kind of the resolved target, from git's own dirs | `f` foreign repo (a DIFFERENT `--git-common-dir`) · `m` the directory does not exist · `o` outside any repo · `w` a LINKED worktree of ours (`--git-dir` ≠ `--git-common-dir`), wherever it sits on disk · `pw` ours (primary **or** worktree) |
95
+ | **V** | do the `@webpieces` versions agree between this worktree and the MAIN tree | `n` skewed · `y` in sync |
96
+ | **R** | command is provably read-only inspection | `n` · `y` |
97
+ | **G** | command invokes git/gh | `n` · `y` |
98
+ | **P** | position of the resolved target | `root` · `sub` |
99
+
100
+ All of them are **Bash only**. Read/Write/Edit resolve their own target (`input.filePath`) and have no
101
+ dimensions — the filter is all that applies to them. **The Read tool is never blocked by L1.**
102
+
103
+ A linked worktree is deliberately **not** foreign: it is the same project, so the guards run against
104
+ THAT tree's branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row 8 below
105
+ is the ONE place they separate, and it turns on **V** — the versions, read off the tree itself.
106
+
107
+ PLACEMENT IS NOT IDENTITY. A worktree checked out INSIDE the repo — `<repo>/.claude/worktrees/agent-XXXX`,
108
+ which is where Claude Code puts every agent worktree — is `w` exactly like a sibling `../feature-dir` one.
109
+ K comes from git's own dirs (`--git-common-dir` is identical for every checkout of one repo,
110
+ `--git-dir` differs only in a linked worktree), never from whether the path sits under the governed root.
111
+ It used to short-circuit on that path test, so an in-repo worktree read as `f` — every bash guard exempt,
112
+ and row 8 unreachable, for the only layout the harness actually produces. A nested clone under
113
+ `repositories/**` still reads `f`, because its shared git dir is its own.
114
+
115
+ `V` comes from reading manifests off disk — the MAIN tree's `pnpm-workspace.yaml` catalog pin, its
116
+ installed `node_modules` version, this worktree's pin, and this worktree's own installed version when
117
+ it has one (which it does the moment anyone runs `pnpm add` there). Three always, a fourth when
118
+ present. Anything unreadable is NO OPINION, never skew: a guard that cannot measure must not block.
119
+ It is deliberately NOT read from `agent_id`/`agent_type` — the dimension this replaced was, and a
120
+ worktree-isolated agent was measured resuming on the primary clone after its tree was reaped, so who
121
+ is asking cannot be trusted to say which tree is being acted on.
122
+
123
+ `R` is `ReadOnlyInspectionScan` — the same paranoid "provably inert" test the unloadable-config escape
124
+ hatch uses (allowlisted viewers/searchers only, no redirects, no `sed -i`).
125
+
126
+ ## Table
127
+
128
+ | # | K | V | R | G | P | act | why | cure |
129
+ |---|---|---|---|---|---|---|---|---|
130
+ | 0 | – | – | – | – | – | 4 block | a `cd` that is not leading + literal, judged before any tree is resolved | `cd <literal abs path> && <the rest>` — ONE leading `cd`, or drop it |
131
+ | 1 | `f` | - | - | - | - | 2 exempt | different git repo — hands off | n/a — not a block |
132
+ | 2 | `o` | - | - | - | - | → L2 | see "Not done" below | n/a — not a block |
133
+ | 8 | `w` | `n` | `n` | - | - | 4 block | this worktree pins a DIFFERENT @webpieces than the main tree that governs it | align the pins (same git hash -> same tracked pin -> install in each tree), work in the main tree, or use a separate clone |
134
+ | 4 | `pw` | - | - | `n` | - | → L2 | force-to-root has no jurisdiction | n/a — not a block |
135
+ | 5 | `pw` | - | - | `y` | `sub` | 4 block | `cd <root> && <original>` | `cd <root> && <original>` |
136
+ | 6 | `pw` | - | - | `y` | `root` | → L2 | | n/a — not a block |
137
+ | 7 | `m` | - | - | - | - | 4 block | the directory is GONE — nothing can run there | `cd <root> && <the work>`, never back through the dead path |
138
+
139
+ Rows 3, 5 and 7 are the structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)
140
+ so they can never be reordered by accident — row 7 (the directory is gone) first, then row 8, then
141
+ force-to-root. Row 7 is printed LAST above only because row numbers are stable across releases and
142
+ renumbering 1-6 would invalidate every `row=` in the logs; `m` matches no other row, so its position
143
+ in the scan is immaterial. All three sit after the L0
144
+ allowlist, after the `f` check, and after the `excludePaths` filter and the config-sync check. So a
145
+ cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0's invariant, and row 8 does
146
+ not weaken it.
147
+
148
+ ## How a log line joins to a row
149
+
150
+ Every L1 decision is written to `.webpieces/logs/L1-location/<writer>.log` with `layer=L1` and
151
+ `row=<n>`, where `<n>` is a row number from the table above. So `row=6` means "this call was judged
152
+ by row 6" and you read the dimensions, the verdict, the reason and the cure straight off that line. Row `0` is
153
+ the pre-stage; it is in the table for exactly this reason.
154
+
155
+ **The join is by DISPATCH, and that is the difference from L2.** L1 takes the FIRST matching row in
156
+ `L1_ROWS` and switches on it, so a row and a behaviour are the same object — delete the row and you
157
+ delete the block. L2's four guard classes each own their own ladder and join to their rows by REASON
158
+ instead (see `webpieces.branch-state-matrix.md`). A totality test walks all 80 classifications and
159
+ asserts each lands on exactly one row, so there is no verdict this page cannot explain.
160
+
161
+ Row numbers are IDENTITY and are never reused: row 3 is retired (coordinator-in-worktree) and row 8
162
+ was added in its place rather than renumbering 4-7, because every `row=` already written to a log
163
+ would otherwise re-point.
164
+
165
+ ## L1 use cases
166
+
167
+ Same row shape as L0: the **Fix** is literal or it is not a fix. `<root>` is the absolute workspace
168
+ root — the messages name it explicitly rather than telling you to `cd` first, for the reason in the
169
+ section head (neither the shell's cwd nor a `cd`'s persistence can be assumed).
170
+
171
+ | # | what you SEE (exact symptom) | state | verdict | Fix |
172
+ |---|---|---|---|---|
173
+ | 1 | `cd repositories/vendored && git commit` goes through untouched | `f` / `y` / - — row 1 | ALLOW_EXEMPT | none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off |
174
+ | 2 | Edit `repositories/vendored/foo.ts` allowed even on stale main | filter — the path is in `excludePaths` | ALLOW_EXEMPT | none needed |
175
+ | 3 | Edit `packages/http/foo.ts` blocked on stale main | filter keeps the rules → L2 fires | BLOCK (at L2) | that is L2's write-on-main verdict, not L1's — follow the L2 message |
176
+ | 4 | Edit `packages/http/foo.ts` judged even though the shell is in `/tmp` | filter, on the TARGET path | → L2 | none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to "fix" it |
177
+ | 5 | `ls` from `packages/http/` runs normally | `pw` / `n` / - — row 4 | ALLOW (handed to L2) | none — force-to-root has no jurisdiction over non-git commands |
178
+ | 6 | `pnpm test` from `packages/http/` runs normally | `pw` / `n` / - — row 4 | ALLOW (handed to L2) | none — deliberately untouched, so package-local test runs stay natural |
179
+ | 7 | `git status` from `packages/http/` is blocked | `pw` / `y` / `sub` — row 5 | BLOCK_AI_CURE | Option 1 (preferred): `cd <root> && git status` |
180
+ | 8 | `cd packages/http && git status` **typed from the root** is blocked | `pw` / `y` / `sub` — row 5 | BLOCK_AI_CURE | Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION |
181
+ | 9 | `cd <root> && git status` passes from anywhere | `pw` / `y` / `root` — row 6 | ALLOW (handed to L2) | none — this IS the prescribed cure |
182
+ | 10 | `echo "cd sub && git push"` passes | `pw` / `n` / `root` — row 4 | ALLOW (handed to L2) | none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape |
183
+ | 11 | `cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one | `pw` / `y` / `sub` — row 5; force-to-root runs first | BLOCK_AI_CURE | Option 1 (preferred): `cd <root> && git push`, which then gets the push guard's real answer ← costs one extra turn by design; still blocked |
184
+ | 12 | a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked | `w` / `n` / `n` — row 8 | BLOCK_AI_CURE | Option 1 (preferred): the MAIN tree is AHEAD, so this is YOURS and it is a one-line edit — raise THIS tree's catalog pin in `pnpm-workspace.yaml` to what main already runs, then `pnpm install` here if this tree has a node_modules. That edit is on the L0 allowlist, so it is typable while the block is up, and nothing has to move in the main tree<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a DIFFERENT version, use a separate CLONE — a clone gets its own governance. That is the answer to "I need a different version", never to "I need to install here": a worktree MAY have its own node_modules (nx, vitest and the eslint plugin all load from it), it just may not hold a different @webpieces version<br>Do NOT: lower the MAIN tree's pin to match — that downgrades every tree, including this session's own governor. And do NOT reach for `pnpm install` BEFORE the edit: this tree's pin is the stale side, so installing first materializes the OLD release |
185
+ | 13 | the same command from a **subagent** runs normally | `w` / `y` — row 8 does not match | ALLOW (handed to L2) | none — a subagent pinned to a worktree is the correct pattern |
186
+ | 14 | inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep` | `w` / `n` / `y` — row 8 does not match | ALLOW (handed to L2) | none — inspection is always open; so are the `Read` tool, `git -C <dir INSIDE this tree> …` and `git show <branch>:<file>`, none of which move you. `git -C <ANOTHER tree>` is a different matter: the harness refuses cross-tree git to a subagent, so it is never the cure for a skew — tell the MAIN agent instead |
187
+ | 15 | `cd <worktree> && pnpm install` still runs while row 8 is live — it is the CURE | L0 allowlist, ahead of L1 | ALLOW | none — a cure must stay reachable from every tree |
188
+ | 16 | a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX` | `w` / `n` / `n` — row 8; in-repo placement is still `w` | BLOCK_AI_CURE | READ THE DIRECTION FIRST — the deny prints it. If the MAIN tree is AHEAD (the common case) a subagent fixes this ITSELF, here, by raising this tree's pin to what main already runs; there is nothing to escalate and the deny prints no escalation. Only when main is BEHIND, or when this branch bumped the pin on purpose, is the subagent stuck — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Then, and only then, forward the deny's verbatim ask to the coordinator and STOP<br>Do NOT: expect exemption because it sits under the repo — K is git's `--git-common-dir` answer, not a path test |
189
+ | 17 | the printed cure REPLACES your `cd`, it does not stack in front of it | `pw` / `y` / `sub` — row 5, on the cure itself | BLOCK_AI_CURE | Option 1 (preferred): run the printed line VERBATIM — `cd <root> && <the work>`, with your own leading `cd` dropped<br>Do NOT: paste `cd <root> && cd <subdir> && <work>`; `effectiveCwd` resolves the leading `cd`s left to right, so that lands in `<subdir>` again and re-fires this exact block |
190
+ | 18 | every command from a worktree another agent REAPED mid-session is blocked | `m` — row 7 | BLOCK_AI_CURE | Option 1 (preferred): run the printed `cd <root> && <the work>` line — it does NOT route back through the dead path<br>Do NOT: re-`cd` into the worktree, or `git worktree add` it back expecting your uncommitted work; that work is gone |
191
+ | 19 | the same block for a NON-git command there — `m` does not care about G | `m` — row 7; K alone decides it | BLOCK_AI_CURE | Option 1 (preferred): the same printed line. A vanished cwd is not a git question — nothing at all can run in a directory that does not exist |
192
+ | 20 | Write `.webpieces/worktrees/agent-*/pr-review/…/review-*.json` allowed on main, with `excludePaths` empty | filter — `.webpieces/` is HARD-CODED exempt (`isWebpiecesStateDir`), ahead of the config list | ALLOW_EXEMPT | none needed — the dir is gitignored, so no config can put it back under governance |
193
+
194
+ Row 8 is the one that changed. It used to be ALLOWED, because the predicate was
195
+ `shellAtRoot || cdsToRoot` — two variables OR'd, so the same destination got opposite verdicts
196
+ depending on where the shell happened to start. It is now one variable, `effectiveCwd === root`.
197
+
198
+ Row 12 is the incident that produced table row 8, and it is a VERSION SKEW incident — which is why
199
+ the row that replaced it measures versions rather than agent identity. The coordinator ran
200
+ `git worktree add`, `cd`'d in,
201
+ and worked there. An L0 version-drift fault then fired against the PRIMARY (pin `0.4.545` vs
202
+ `node_modules` `0.4.526`) and prescribed `pnpm install` — which ran in the WORKTREE, internally
203
+ consistent at `0.4.526`/`0.4.526`, so it succeeded, changed nothing in the measured tree, and the guard
204
+ re-denied. Five identical installs later the agent had invented a theory about the harness stripping
205
+ its `cd` and handed the problem to the human. Note what row 15 says: the fix is NOT to deny that
206
+ install. It is to make the split state unreachable, so the wrong-tree install is never plausible.
207
+
208
+ ## Not done — `o` is not exempt yet
209
+
210
+ Row 2 hands `'outside'` down to L2 rather than exempting it. `'outside'` is produced at
211
+ `effective-tree.ts` (git has no answer for the directory) carrying `governedRoot`, and **no code branches on it**, so a
212
+ command in no git repo is judged against the governed repo's branch and staleness state. That is a
213
+ wrong verdict, and `exempt` is the right action.
214
+
215
+ **It must not ship alone.** Jurisdiction comes from the shell cwd, not from what the command touches,
216
+ so exempting `o` opens a bypass an agent reaches by typing `cd /tmp &&`:
217
+
218
+ | command | today | with `o → exempt` alone |
219
+ |---|---|---|
220
+ | `cd /tmp && ls` | judged against the repo | exempt — **correct** |
221
+ | `cd /tmp && git -C $REPO commit` | L2 guards fire | exempt — **every L2 guard bypassed** |
222
+ | `cd /tmp && rm -rf $REPO/packages/http/src` | judged | exempt — **unguarded** |
223
+
224
+ The two cases only separate once jurisdiction is judged on **what the command touches** (explicit
225
+ `git -C` / `--work-tree`, then path arguments, then the `cd`, then the shell cwd), with the fail-safe
226
+ rule that **any** resolved target inside `governedRoot` means `pw`. Ship the two together, or neither.
227
+
228
+ Tracked in `backlog/bug-bash-guards-judge-the-shell-cwd-not-the-paths-the-command-touches.md` and
229
+ `backlog/bug-outside-tree-kind-is-never-consumed-so-a-non-git-dir-is-judged-against-the-governed-repo.md`.
230
+ That resolver has three consumers — L1's K, L2's scope dimension, and `excludePaths` on the Bash path
231
+ — which is why the backlog says **fix once**.
232
+
233
+ ---
234
+
235
+
236
+ ## Code anchors
237
+
238
+ | section | file | symbol |
239
+ |---|---|---|
240
+ | resolver, K | `ai-hook-rules/src/core/effective-tree.ts` | `EffectiveTreeResolver`, `TreeKind` |
241
+ | the two structural blocks, in order | `ai-hook-rules/src/core/runner.ts` | `l1LocationBlock` |
242
+ | trinary-version-skew (row 8), V, R | `ai-hook-rules/src/core/version-sync.ts` | `VersionSyncGuard`, `WebpiecesVersions` |
243
+ | force-to-root (row 5) | `ai-hook-rules/src/core/force-to-root.ts` | `ForceToRootGuard` |
244
+ | the directory is gone (row 7) | `ai-hook-rules/src/core/missing-directory.ts` | `MissingDirectoryGuard` |
245
+ | the filter | `ai-hook-rules/src/core/runner.ts` | `filterByExcludedPaths` |
246
+ | `excludePaths` shape | `rules-config/src/exclude-hook-paths.ts`, `validate-config.ts`, `retired-config-keys.ts` | `ExcludePaths`, `validateExcludePaths` |
247
+ | the `.webpieces/` skip | `rules-config/src/exclude-hook-paths.ts` | `isWebpiecesStateDir` |