@webpieces/tooling-common 0.0.1
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 +31 -0
- package/src/atomic-file.d.ts +46 -0
- package/src/atomic-file.js +114 -0
- package/src/atomic-file.js.map +1 -0
- package/src/claude-config-dir.d.ts +62 -0
- package/src/claude-config-dir.js +87 -0
- package/src/claude-config-dir.js.map +1 -0
- package/src/index.d.ts +6 -0
- package/src/index.js +10 -0
- package/src/index.js.map +1 -0
- package/src/inform-ai-error.d.ts +6 -0
- package/src/inform-ai-error.js +13 -0
- package/src/inform-ai-error.js.map +1 -0
- package/src/state-dir-migration.d.ts +79 -0
- package/src/state-dir-migration.js +185 -0
- package/src/state-dir-migration.js.map +1 -0
- package/src/state-dir.d.ts +209 -0
- package/src/state-dir.js +349 -0
- package/src/state-dir.js.map +1 -0
- package/src/state-path-constants.d.ts +2 -0
- package/src/state-path-constants.js +6 -0
- package/src/state-path-constants.js.map +1 -0
- package/src/to-error.d.ts +1 -0
- package/src/to-error.js +10 -0
- package/src/to-error.js.map +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StateDirMigrator = exports.StateMigrationReport = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const inversify_1 = require("inversify");
|
|
8
|
+
const to_error_1 = require("./to-error");
|
|
9
|
+
/**
|
|
10
|
+
* What a migration did. Data-only (per CLAUDE.md: classes for data, explicit construction).
|
|
11
|
+
*
|
|
12
|
+
* `kept` is the important half: an entry that could NOT be moved because the destination already holds
|
|
13
|
+
* something at that path. Nothing in `kept` is ever deleted — it stays where it is, for a human.
|
|
14
|
+
*/
|
|
15
|
+
class StateMigrationReport {
|
|
16
|
+
moved = [];
|
|
17
|
+
kept = [];
|
|
18
|
+
get movedAnything() {
|
|
19
|
+
return this.moved.length > 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.StateMigrationReport = StateMigrationReport;
|
|
23
|
+
/**
|
|
24
|
+
* Moves a LEGACY per-worktree `.webpieces/` REAL DIRECTORY into the worktree's namespace inside the
|
|
25
|
+
* primary clone (`<primary>/.webpieces/worktrees/<name>/`).
|
|
26
|
+
*
|
|
27
|
+
* NO SYMLINK IS EVER CREATED in its place — an earlier draft of this design planned one and this
|
|
28
|
+
* comment still said so, which is worse than silence: it told the reader to expect an indirection
|
|
29
|
+
* that does not exist. `DotWebpieces` resolves the namespace path EXPLICITLY (see its "Why two
|
|
30
|
+
* explicit methods and not a symlink" section, which records why the link was rejected: lazy creation
|
|
31
|
+
* with three failure modes, a Windows hazard, and `rename(2)` silently REPLACING the link). After a
|
|
32
|
+
* migration the legacy `<worktree>/.webpieces/` is simply gone; nothing takes its place.
|
|
33
|
+
*
|
|
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.
|
|
42
|
+
*
|
|
43
|
+
* WHY a plain move suffices: the destination namespace is created FOR this worktree, so it is empty in
|
|
44
|
+
* the ordinary case and the whole tree moves in one `rename`. Where something is already there (a
|
|
45
|
+
* repeat run, or an old PUBLISHED build having written to the legacy path again mid-transition), we
|
|
46
|
+
* descend and move only what is free.
|
|
47
|
+
*
|
|
48
|
+
* SAFETY RULE, absolute: this never deletes or overwrites anything that holds content. If a
|
|
49
|
+
* destination path is occupied, the legacy copy is LEFT WHERE IT IS and reported loudly on stderr.
|
|
50
|
+
* Empty directories are removed as they drain, which is how a fully-migrated legacy `.webpieces/`
|
|
51
|
+
* disappears on its own and lets the symlink be created.
|
|
52
|
+
*
|
|
53
|
+
* It is also the answer to the PUBLISHED-vs-LOCAL transition window. The `wp-*` bins and the hooks run
|
|
54
|
+
* the PUBLISHED package, so for a while some invocations still create a REAL `<worktree>/.webpieces`
|
|
55
|
+
* directory. Because this runs on the first state-dir resolution of EVERY new-code process, anything an
|
|
56
|
+
* old-code invocation deposited is swept into the worktree namespace before any reader looks — and a
|
|
57
|
+
* reader running old code finds it through the symlink either way. The two schemes converge instead of
|
|
58
|
+
* splitting; nothing is lost in either direction.
|
|
59
|
+
*/
|
|
60
|
+
let StateDirMigrator = class StateDirMigrator {
|
|
61
|
+
/**
|
|
62
|
+
* Drain `legacyDir` into `targetDir`. A no-op when they are the same directory or when the legacy
|
|
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.
|
|
70
|
+
*/
|
|
71
|
+
migrate(legacyDir, targetDir, keepInPlace) {
|
|
72
|
+
const report = new StateMigrationReport();
|
|
73
|
+
if (path.resolve(legacyDir) === path.resolve(targetDir))
|
|
74
|
+
return report;
|
|
75
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
76
|
+
try {
|
|
77
|
+
if (!this.isRealDirectory(legacyDir))
|
|
78
|
+
return report;
|
|
79
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
80
|
+
this.drain(legacyDir, targetDir, '', report, new Set(keepInPlace));
|
|
81
|
+
this.removeIfEmpty(legacyDir);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const error = (0, to_error_1.toError)(err);
|
|
85
|
+
this.warn(`could not migrate ${legacyDir} into ${targetDir}: ${error.message}`);
|
|
86
|
+
return report;
|
|
87
|
+
}
|
|
88
|
+
this.announce(legacyDir, targetDir, report);
|
|
89
|
+
return report;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Move everything under `<legacyRoot>/<relative>` to `<targetRoot>/<relative>`.
|
|
93
|
+
*
|
|
94
|
+
* A whole subtree whose destination is free moves in ONE `rename` — which is what keeps an
|
|
95
|
+
* in-flight `merge-info/staged/<branch>/` intact rather than copying it file by file and risking a
|
|
96
|
+
* half-moved merge. Only when the destination is an existing DIRECTORY do we descend and consider
|
|
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.
|
|
102
|
+
*/
|
|
103
|
+
drain(legacyRoot, targetRoot, relative, report, keepInPlace) {
|
|
104
|
+
const from = path.join(legacyRoot, relative);
|
|
105
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
106
|
+
if (relative === '' && keepInPlace.has(entry.name))
|
|
107
|
+
continue;
|
|
108
|
+
const childRelative = path.join(relative, entry.name);
|
|
109
|
+
const source = path.join(legacyRoot, childRelative);
|
|
110
|
+
const destination = path.join(targetRoot, childRelative);
|
|
111
|
+
if (!fs.existsSync(destination)) {
|
|
112
|
+
this.relocate(source, destination, childRelative, report);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (entry.isDirectory() && fs.statSync(destination).isDirectory()) {
|
|
116
|
+
this.drain(legacyRoot, targetRoot, childRelative, report, keepInPlace);
|
|
117
|
+
this.removeIfEmpty(source);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
report.kept.push(childRelative);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Move one file or subtree to a free destination. `rename` first (atomic, instant, and the only
|
|
124
|
+
// form that cannot half-move a merge); a cross-device rename falls back to copy-then-remove.
|
|
125
|
+
relocate(source, destination, relative, report) {
|
|
126
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
127
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
128
|
+
try {
|
|
129
|
+
fs.renameSync(source, destination);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const error = (0, to_error_1.toError)(err);
|
|
133
|
+
void error;
|
|
134
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
135
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
report.moved.push(relative);
|
|
138
|
+
}
|
|
139
|
+
// A real directory, not a symlink to one — `lstat` on purpose. A symlink here is the NEW scheme
|
|
140
|
+
// already in place, and following it would make the migrator try to drain a directory into itself.
|
|
141
|
+
isRealDirectory(dir) {
|
|
142
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
143
|
+
try {
|
|
144
|
+
return fs.lstatSync(dir).isDirectory();
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const error = (0, to_error_1.toError)(err);
|
|
148
|
+
void error;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Remove a directory only when it is genuinely empty. Never recursive — a non-empty legacy dir is
|
|
153
|
+
// state a human still needs.
|
|
154
|
+
removeIfEmpty(dir) {
|
|
155
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
156
|
+
try {
|
|
157
|
+
if (!fs.existsSync(dir))
|
|
158
|
+
return;
|
|
159
|
+
if (fs.readdirSync(dir).length > 0)
|
|
160
|
+
return;
|
|
161
|
+
fs.rmdirSync(dir);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const error = (0, to_error_1.toError)(err);
|
|
165
|
+
void error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Migration is invisible by design when it goes cleanly; anything LEFT BEHIND must be shouted about,
|
|
169
|
+
// because that is the only case where a human has to do something.
|
|
170
|
+
announce(legacyDir, targetDir, report) {
|
|
171
|
+
if (report.kept.length === 0)
|
|
172
|
+
return;
|
|
173
|
+
this.warn(`${String(report.kept.length)} item(s) in ${legacyDir} could NOT move into ${targetDir} because ` +
|
|
174
|
+
`that path is already occupied. NOTHING was deleted — resolve by hand if any of these still ` +
|
|
175
|
+
`matters (an in-flight merge, or log history you have not read): ${report.kept.join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
warn(message) {
|
|
178
|
+
process.stderr.write(`[webpieces] .webpieces state migration: ${message}\n`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
exports.StateDirMigrator = StateDirMigrator;
|
|
182
|
+
exports.StateDirMigrator = StateDirMigrator = tslib_1.__decorate([
|
|
183
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
184
|
+
], StateDirMigrator);
|
|
185
|
+
//# sourceMappingURL=state-dir-migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-dir-migration.js","sourceRoot":"","sources":["../../../../../packages/tooling/tooling-common/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"]}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { StateDirMigrator } from './state-dir-migration';
|
|
2
|
+
export declare const WORKTREE_STATE_DIR = "worktrees";
|
|
3
|
+
export declare const LOGS_STATE_DIR = "logs";
|
|
4
|
+
export declare const AI_WRITABLE_STATE_DIRS: readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* Data-only carrier for the two paths git is asked for. Per CLAUDE.md: classes for data.
|
|
7
|
+
*
|
|
8
|
+
* `gitDir` is the PER-WORKTREE git dir (`<primary>/.git/worktrees/<name>` in a linked worktree,
|
|
9
|
+
* `<primary>/.git` in the primary clone). `commonDir` is the SHARED one — always `<primary>/.git`.
|
|
10
|
+
* They differ if and only if this is a linked worktree; that is git's own canonical test.
|
|
11
|
+
*/
|
|
12
|
+
export declare class GitDirs {
|
|
13
|
+
readonly gitDir: string;
|
|
14
|
+
readonly commonDir: string;
|
|
15
|
+
constructor(gitDir: string, commonDir: string);
|
|
16
|
+
get isLinkedWorktree(): boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* WHERE a piece of `.webpieces/` state belongs — the ONE resolver every reader and writer must go
|
|
20
|
+
* through, with THREE named methods because there are exactly three answers and a call site must DECLARE
|
|
21
|
+
* which one it means.
|
|
22
|
+
*
|
|
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)
|
|
28
|
+
*
|
|
29
|
+
* ─── The bug ───────────────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
* `.webpieces/` is gitignored, and it was anchored at the directory holding webpieces.config.json —
|
|
31
|
+
* which in a linked worktree is the WORKTREE. So a repo with seven worktrees had SEVEN independent
|
|
32
|
+
* copies of files that describe the WHOLE REPO. `merged-branches.json` holds verdicts for every branch
|
|
33
|
+
* AND every worktree in the repo; N copies is N divergent truths, and the guards read them as fact.
|
|
34
|
+
* Observed in the field: branch-creation-guard asserted "8 parked local branches" while
|
|
35
|
+
* `git branch --list` showed ONE — it was reading a cache written before deletions performed from a
|
|
36
|
+
* DIFFERENT worktree — and it then blocked a legitimate `git worktree add` on that fiction.
|
|
37
|
+
*
|
|
38
|
+
* ─── Why two explicit methods and not a symlink ────────────────────────────────────────────────────
|
|
39
|
+
* A `<worktree>/.webpieces` → `<primary>/…/worktrees/<name>` symlink would have left every call site
|
|
40
|
+
* untouched, which is seductive and wrong. Nothing hooks `git worktree add`, so the link needs LAZY
|
|
41
|
+
* creation that also has to handle "link already exists", "link points somewhere else", and "a real
|
|
42
|
+
* directory is already there" — invisible filesystem magic with a Windows failure mode. Worse, the safe
|
|
43
|
+
* way to write a shared file under concurrency is temp-file-then-`rename()`, and `rename(2)` acts on
|
|
44
|
+
* the PATH, not the link: it REPLACES a symlink with a real file. That silently works for one writer
|
|
45
|
+
* and diverges for everyone else — the exact bug being fixed, but invisible. An explicit call is
|
|
46
|
+
* greppable, testable, and forces each site to say which scope it means.
|
|
47
|
+
*
|
|
48
|
+
* ─── Which scope is which (the scope assignment is deliberate, not incidental) ─────────────────────
|
|
49
|
+
* shared():
|
|
50
|
+
* • merged-branches.json — verdicts for every branch and worktree in the repo. Atomically written.
|
|
51
|
+
* • main-sync-status.json AND main-sync.lock.json — there is one `main` and one `.git`, so there is
|
|
52
|
+
* one refresher. A lock inside a per-worktree directory locks nothing.
|
|
53
|
+
* local():
|
|
54
|
+
* • logs/*.log, INCLUDING branch-mutations.log. A shared append-only log genuinely corrupts:
|
|
55
|
+
* `O_APPEND` writes are indivisible only under PIPE_BUF, which is 512 bytes on macOS, and a
|
|
56
|
+
* `recover=git worktree add -b <branch> <abs-path> <tag>` line with real paths exceeds that. A
|
|
57
|
+
* per-worktree log has exactly ONE writer and cannot tear, and under this layout it already
|
|
58
|
+
* survives the worktree's deletion — recovery is one glob over
|
|
59
|
+
* `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.
|
|
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: `summary.json` is authored by the agent running the flow, and `review-round<N>-<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-round<N>-<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().
|
|
89
|
+
*
|
|
90
|
+
* ─── The boundary that must not be blurred ─────────────────────────────────────────────────────────
|
|
91
|
+
* ONLY the gitignored `.webpieces/` STATE relocates. `webpieces.config.json` is TRACKED IN GIT and is
|
|
92
|
+
* therefore part of the BRANCH: a branch may legitimately change its own rules and that must keep
|
|
93
|
+
* working. Config resolution is untouched — still per-worktree, via findConfigFile /
|
|
94
|
+
* RepoRootFinder.resolveRepoRoot. Nothing in this class reads or moves config.
|
|
95
|
+
*
|
|
96
|
+
* ─── Why `--git-dir` / `--git-common-dir`, and not one of the existing services ────────────────────
|
|
97
|
+
* They are git's own answers, from any subdirectory, in one cheap local call, with no `.git`-file
|
|
98
|
+
* parsing by hand (which gets `--separate-git-dir` and submodules wrong). Two existing mechanisms do
|
|
99
|
+
* distinguish primary from linked, and neither is the right authority here:
|
|
100
|
+
* • `WorktreeService` answers "what worktrees exist and what do they hold" — a repo-wide ENUMERATION
|
|
101
|
+
* (`git worktree list --porcelain`) for the caps and the reaper. Using it for a path lookup would
|
|
102
|
+
* run an enumeration on the hook's blocking path for every state access, and it fails SOFT to `[]`,
|
|
103
|
+
* which here would read as "there is no primary clone" on exactly the degraded repo where the
|
|
104
|
+
* answer matters most. It also does not expose the worktree's git NAME, which is the namespace key.
|
|
105
|
+
* • `EffectiveTreeResolver` (#524) answers "which tree does this COMMAND act on" — it takes a command
|
|
106
|
+
* string and is a policy input to the bash guards, not a filesystem-path resolver.
|
|
107
|
+
* Both remain authoritative for their own questions. This asks the narrowest one — two path strings —
|
|
108
|
+
* and lives in rules-config, UNDER both, which is where a primitive that pr-gate, ai-hook-rules and
|
|
109
|
+
* code-rules all need has to sit.
|
|
110
|
+
*
|
|
111
|
+
* Fails CLOSED to the pre-change behaviour: when git cannot answer, every path here collapses to
|
|
112
|
+
* `<startDir-root>/.webpieces` exactly as before. Degrading to merely-suboptimal beats throwing on a
|
|
113
|
+
* hook's blocking path.
|
|
114
|
+
*/
|
|
115
|
+
export declare class DotWebpieces {
|
|
116
|
+
private readonly migrator;
|
|
117
|
+
private readonly gitDirsByRoot;
|
|
118
|
+
private readonly treeRootByDir;
|
|
119
|
+
private readonly migrated;
|
|
120
|
+
constructor(migrator?: StateDirMigrator);
|
|
121
|
+
/**
|
|
122
|
+
* REPO-WIDE state: `<primary>/.webpieces`. Use ONLY for facts about the repo rather than about one
|
|
123
|
+
* worktree — today that is merged-branches.json and the main-sync status + lock. Identical from
|
|
124
|
+
* every worktree, and never behind an indirection, so an atomic `rename()` into it is safe.
|
|
125
|
+
*/
|
|
126
|
+
shared(startDir: string): string;
|
|
127
|
+
/** A path beneath the repo-wide state dir. */
|
|
128
|
+
sharedFile(startDir: string, ...segments: string[]): string;
|
|
129
|
+
/**
|
|
130
|
+
* THIS WORKTREE's private state: `<primary>/.webpieces/worktrees/<name>` for a linked worktree, and
|
|
131
|
+
* `<primary>/.webpieces` for the primary clone, which keeps its state exactly where it has always
|
|
132
|
+
* been. Fully isolated — two worktrees never write the same path, so nothing here needs a lock.
|
|
133
|
+
*
|
|
134
|
+
* The first call for a linked worktree also MIGRATES a legacy real `<worktree>/.webpieces/`
|
|
135
|
+
* directory into the namespace, so in-flight merge / pr-review state written under the old scheme
|
|
136
|
+
* (or by an older PUBLISHED build during the transition) is picked up rather than orphaned.
|
|
137
|
+
*/
|
|
138
|
+
local(startDir: string): string;
|
|
139
|
+
/** A path beneath this worktree's private state dir. */
|
|
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;
|
|
152
|
+
/**
|
|
153
|
+
* THE log directory — `<local()>/logs` — and the only place webpieces writes a `.log` or a log's
|
|
154
|
+
* detail files, in the primary clone and in every worktree namespace alike.
|
|
155
|
+
*
|
|
156
|
+
* There is NO migration here, deliberately. A lazy once-per-process relocation of an older
|
|
157
|
+
* release's `hooks/*.log` used to run on this path, and it was itself the defect it claimed to
|
|
158
|
+
* cure: it only fires in the trees a process happens to re-enter, so it guarantees two answers to
|
|
159
|
+
* "where are the logs" indefinitely rather than converging on one. New writes go to `logs/` and
|
|
160
|
+
* nothing else; a stale `hooks/` left by an older release is inert and can be deleted by hand.
|
|
161
|
+
*/
|
|
162
|
+
logs(startDir: string): string;
|
|
163
|
+
/** A path beneath the log directory — `dotWebpieces.logsFile(root, CALLS_STREAM, writerFile)`. */
|
|
164
|
+
logsFile(startDir: string, ...segments: string[]): string;
|
|
165
|
+
/** True when `startDir` sits in a LINKED worktree rather than the primary clone. */
|
|
166
|
+
isLinkedWorktree(startDir: string): boolean;
|
|
167
|
+
/**
|
|
168
|
+
* git's two dir answers for `startDir`, memoized — null when it is not a git repo / git is
|
|
169
|
+
* unavailable.
|
|
170
|
+
*
|
|
171
|
+
* PUBLIC because TREE IDENTITY is decided from these two strings and nothing else. `commonDir`
|
|
172
|
+
* is the same for every checkout of ONE repo, so `commonDir(a) === commonDir(b)` is "same repo,
|
|
173
|
+
* different placement" — the test that tells a linked worktree (ours, wherever it sits, including
|
|
174
|
+
* inside the repo at `.claude/worktrees/**`) from a nested clone under `repositories/**` (not
|
|
175
|
+
* ours). `gitDirs.isLinkedWorktree` then separates primary from linked. See
|
|
176
|
+
* `EffectiveTreeResolver.classify`, the caller that runs this on the hook's blocking path, which is
|
|
177
|
+
* why it goes through this cache rather than spawning its own `rev-parse`.
|
|
178
|
+
*/
|
|
179
|
+
gitDirs(startDir: string): GitDirs | null;
|
|
180
|
+
/**
|
|
181
|
+
* The WORKING TREE root containing `startDir` (`git rev-parse --show-toplevel`), memoized, or null
|
|
182
|
+
* when `startDir` is not in a git repo. In a linked worktree this is the WORKTREE's own root, which
|
|
183
|
+
* is exactly what a caller judging "which checkout am I standing in" needs.
|
|
184
|
+
*
|
|
185
|
+
* THE ONE tree-root resolver. There used to be three byte-identical `spawnSync … --show-toplevel`
|
|
186
|
+
* copies — this one, `RepoRootFinder`'s and `effective-tree.ts`'s — each commenting that it
|
|
187
|
+
* "mirrors" one of the others, and one of those comments was already stale. Both copies are deleted;
|
|
188
|
+
* everything that needs a tree root calls this. Do not add a fourth.
|
|
189
|
+
*/
|
|
190
|
+
treeRoot(startDir: string): string | null;
|
|
191
|
+
/**
|
|
192
|
+
* git's own name for this linked worktree — the basename of `<primary>/.git/worktrees/<name>`, and
|
|
193
|
+
* the namespace key under `worktrees/`. Empty for the primary clone. git's name rather than the
|
|
194
|
+
* directory's basename, so two worktrees checked out into same-named directories under different
|
|
195
|
+
* parents cannot collide.
|
|
196
|
+
*/
|
|
197
|
+
worktreeName(startDir: string): string;
|
|
198
|
+
/** The primary clone's root, from any worktree. Falls back to git's toplevel-less best guess. */
|
|
199
|
+
primaryRoot(startDir: string): string;
|
|
200
|
+
/**
|
|
201
|
+
* The PRE-change location, `<treeRoot>/.webpieces` — what every call site used to compute. Public
|
|
202
|
+
* because the migrator and its specs must be able to name the thing being migrated FROM, and
|
|
203
|
+
* because the transition-window fallback readers need it.
|
|
204
|
+
*/
|
|
205
|
+
legacyDir(treeRoot: string): string;
|
|
206
|
+
private migrateOnce;
|
|
207
|
+
private revParse;
|
|
208
|
+
}
|
|
209
|
+
export declare const dotWebpieces: DotWebpieces;
|