@webpieces/rules-config 0.4.584 → 0.4.587
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.587",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -28,7 +28,16 @@ export declare class BranchMutationLog {
|
|
|
28
28
|
* PIPE_BUF, which is 512 bytes on macOS, and a REAP_WORKTREE line carrying
|
|
29
29
|
* `recover=git worktree add -b <branch> <absolute-path> <tag>` exceeds that — concurrent appenders
|
|
30
30
|
* from seven worktrees would interleave into an unrecoverable audit trail, which is the one thing
|
|
31
|
-
* this file exists not to be. Per-worktree
|
|
31
|
+
* this file exists not to be. Per-worktree, THIS log has one writer and cannot tear.
|
|
32
|
+
*
|
|
33
|
+
* That last claim is narrower than it looks, so do not generalise it (2026-08-06). It holds here
|
|
34
|
+
* because the wp-* bins write this file one command at a time. It does NOT hold for the ai-hook
|
|
35
|
+
* logs in the same directory: Claude Code runs every matching PreToolUse hook IN PARALLEL, and
|
|
36
|
+
* several agents and sessions can share one tree, so a per-worktree name there had several
|
|
37
|
+
* concurrent writers. Those names now carry a `<sessionId>-<agentId|coordinator>-<hook>-` prefix
|
|
38
|
+
* (ai-hook-rules' LogStream). This file keeps a bare name deliberately — LogStream lives in
|
|
39
|
+
* ai-hook-rules, which DEPENDS on rules-config, so the import direction forbids reusing it here.
|
|
40
|
+
* If this log ever gains a second concurrent writer, it needs its own stream identity first.
|
|
32
41
|
*
|
|
33
42
|
* Nothing is lost by keeping it local: under the `worktrees/<name>/` layout the log lives in the
|
|
34
43
|
* PRIMARY clone, so it survives `git worktree remove`, and the whole history is one glob —
|
|
@@ -65,7 +65,16 @@ let BranchMutationLog = class BranchMutationLog {
|
|
|
65
65
|
* PIPE_BUF, which is 512 bytes on macOS, and a REAP_WORKTREE line carrying
|
|
66
66
|
* `recover=git worktree add -b <branch> <absolute-path> <tag>` exceeds that — concurrent appenders
|
|
67
67
|
* from seven worktrees would interleave into an unrecoverable audit trail, which is the one thing
|
|
68
|
-
* this file exists not to be. Per-worktree
|
|
68
|
+
* this file exists not to be. Per-worktree, THIS log has one writer and cannot tear.
|
|
69
|
+
*
|
|
70
|
+
* That last claim is narrower than it looks, so do not generalise it (2026-08-06). It holds here
|
|
71
|
+
* because the wp-* bins write this file one command at a time. It does NOT hold for the ai-hook
|
|
72
|
+
* logs in the same directory: Claude Code runs every matching PreToolUse hook IN PARALLEL, and
|
|
73
|
+
* several agents and sessions can share one tree, so a per-worktree name there had several
|
|
74
|
+
* concurrent writers. Those names now carry a `<sessionId>-<agentId|coordinator>-<hook>-` prefix
|
|
75
|
+
* (ai-hook-rules' LogStream). This file keeps a bare name deliberately — LogStream lives in
|
|
76
|
+
* ai-hook-rules, which DEPENDS on rules-config, so the import direction forbids reusing it here.
|
|
77
|
+
* If this log ever gains a second concurrent writer, it needs its own stream identity first.
|
|
69
78
|
*
|
|
70
79
|
* Nothing is lost by keeping it local: under the `worktrees/<name>/` layout the log lives in the
|
|
71
80
|
* PRIMARY clone, so it survives `git worktree remove`, and the whole history is one glob —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-mutation-log.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/branch-mutation-log.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 { DotWebpieces, dotWebpieces } from './state-dir';\nimport { toError } from './to-error';\n\n// The BRANCH-MUTATION log — an audit trail for every workflow verb that RENAMES or MOVES branches.\n// Records START / each phase boundary / END-with-outcome so the next agent (or a human) can\n// reconstruct what the tooling did to the branches. Writes to `.webpieces/logs/branch-mutations.log`\n// (in a linked worktree: `.webpieces/worktrees/<name>/logs/`) — see LOGS_STATE_DIR for why every\n// webpieces log lives under `logs/` and no longer beside the non-log state in `hooks/`.\n// Lives in rules-config (the shared dep of pr-gate) so the pr-gate scripts can call it directly.\n\nconst LOG_FILE = 'branch-mutations.log';\nconst LOG_FILE_PREV = 'branch-mutations.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors the other webpieces logs)\nconst MAX_DETAIL_LEN = 400;\n\n// The workflow verb whose branch mutation is being logged (the bin the AI/human invoked).\n// `auto-reap` is the odd one out: no human invoked it — it is the detached background refresher\n// (sync-main.ts) deleting dead branches on its own. It gets a verb precisely BECAUSE it is\n// unattended: a deletion nobody watched happen is the one that most needs an audit line.\nexport type MutationVerb =\n | 'wp-start-update' | 'wp-finish-update' | 'wp-start-upsert-pr' | 'wp-review-upsert-pr'\n | 'wp-finish-upsert-pr' | 'wp-cleanup' | 'wp-land-pr' | 'auto-reap';\n\n// A boundary within a verb's execution. START/END bracket the whole run; the middle phases mark each\n// irreversible git step so an interrupt leaves a breadcrumb at the last phase reached.\n// REAP is a whole mutation in one line (a branch delete has no phases) — see BranchReaper.\n// REAP_WORKTREE is its worktree twin: archive → `git worktree remove` → `git branch -D`, all three of\n// which succeed or fail as one act. It is a SEPARATE phase, not just a REAP with a path, so that\n// `grep REAP_WORKTREE` answers \"what directories did the tooling delete?\" — a strictly scarier\n// question than \"what refs did it delete?\", since a worktree removal takes real files with it.\nexport type MutationPhase =\n | 'START' | 'BACKUP' | 'CHECKOUT_MAIN' | 'PULL' | 'SQUASH' | 'RENAME'\n | 'FINALIZE' | 'CONFLICT' | 'INTERRUPTED' | 'END' | 'REAP' | 'REAP_WORKTREE';\n\n// Data-only record of one branch-mutation event (per CLAUDE.md: classes for data, explicit construction).\nexport class BranchMutationEvent {\n verb: MutationVerb;\n phase: MutationPhase;\n fromBranch: string = '';\n toBranch: string = '';\n oldMain: string = '';\n newMain: string = '';\n conflict: boolean = false;\n conflictFiles: string[] = [];\n outcome: string = '';\n artifacts: string[] = [];\n // The commit a DELETED branch pointed at, captured immediately before the delete. This is what\n // makes a reap auditable AND reversible: the work is already in main, and the pre-delete tip is\n // still addressable by hash (the reflog holds it ~90 days), so formatDetail renders a literal\n // `recover=git branch <name> <sha>` next to it. Empty for mutations that delete nothing.\n sha: string = '';\n // The `archive/<date>/<branch>` tag written immediately BEFORE a REAP deleted the branch. When set,\n // formatDetail renders `recover=` against the TAG instead of the sha: a tag is a permanent ref that\n // survives `gc` and reflog expiry and can be pushed, whereas a bare sha is only recoverable while\n // this clone's reflog still holds it. Empty when nothing was tagged (retention policy 'delete').\n archiveTag: string = '';\n // The directory a REAP_WORKTREE removed. When set, `recover=` becomes the WORKTREE form\n // (`git worktree add -b <branch> <path> <ref>`) rather than the bare `git branch` form: putting the\n // ref back does not put the directory back, and a recover line that restores half of what was\n // destroyed is worse than none — it reads as done. Empty for every mutation that removes no directory.\n worktreePath: string = '';\n\n constructor(verb: MutationVerb, phase: MutationPhase) {\n this.verb = verb;\n this.phase = phase;\n }\n}\n\n/** Appends branch-mutation audit lines. `@injectable(bindingScopeValues.Singleton)` so it's injectable + drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class BranchMutationLog {\n constructor(private readonly dotDir: DotWebpieces = dotWebpieces) {}\n\n /**\n * LOCAL scope, deliberately — one log per worktree, not one per repo.\n *\n * A SHARED append-only log would genuinely corrupt. `O_APPEND` makes a write indivisible only up to\n * PIPE_BUF, which is 512 bytes on macOS, and a REAP_WORKTREE line carrying\n * `recover=git worktree add -b <branch> <absolute-path> <tag>` exceeds that — concurrent appenders\n * from seven worktrees would interleave into an unrecoverable audit trail, which is the one thing\n * this file exists not to be. Per-worktree it has exactly ONE writer and cannot tear.\n *\n * Nothing is lost by keeping it local: under the `worktrees/<name>/` layout the log lives in the\n * PRIMARY clone, so it survives `git worktree remove`, and the whole history is one glob —\n * `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.\n */\n branchMutationLogPath(root: string): string {\n return this.dotDir.logsFile(root, LOG_FILE);\n }\n\n /**\n * Append one tab-separated line per branch-mutation event to\n * `.webpieces/logs/branch-mutations.log`. Swallows all errors — logging must NEVER block or fail\n * the workflow it is observing.\n */\n logBranchMutation(root: string, event: BranchMutationEvent): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n const logsDir = this.dotDir.logs(root);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, LOG_FILE);\n this.rotateLogFile(logPath, path.join(logsDir, LOG_FILE_PREV));\n\n const line = [\n `[${timestamp}]`,\n event.verb,\n event.phase,\n this.oneLine(this.formatDetail(event)),\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // Render only the fields this event actually set, as `key=value` tokens — greppable on one line.\n private formatDetail(event: BranchMutationEvent): string {\n const parts: string[] = [];\n // A rename/move has both ends; a REAP has only the branch it destroyed. Printing `to=?` for\n // the latter reads like a lost destination rather than \"there was never one\".\n if (event.fromBranch !== '' && event.toBranch !== '') parts.push(`from=${event.fromBranch} to=${event.toBranch}`);\n else if (event.fromBranch !== '') parts.push(`branch=${event.fromBranch}`);\n else if (event.toBranch !== '') parts.push(`from=? to=${event.toBranch}`);\n if (event.oldMain !== '' || event.newMain !== '') parts.push(`oldMain=${event.oldMain || '?'} newMain=${event.newMain || '?'}`);\n if (event.conflict) parts.push('conflict=true');\n if (event.conflictFiles.length > 0) parts.push(`conflictFiles=${event.conflictFiles.length}(${event.conflictFiles.join(',')})`);\n if (event.outcome !== '') parts.push(`outcome=${event.outcome}`);\n // Emitted as one unit so the hash is never separated from the command that undoes the delete.\n // Prefer the archive TAG as the recover ref when there is one — it does not expire.\n if (event.worktreePath !== '') {\n parts.push(this.worktreeDetail(event));\n } else if (event.sha !== '' && event.archiveTag !== '') {\n parts.push(\n `sha=${event.sha} archiveTag=${event.archiveTag} ` +\n `recover=git checkout -b ${event.fromBranch || '?'} ${event.archiveTag}`,\n );\n } else if (event.sha !== '') {\n parts.push(`sha=${event.sha} recover=git branch ${event.fromBranch || '?'} ${event.sha}`);\n }\n for (const artifact of event.artifacts) parts.push(`artifact=${artifact}`);\n return parts.join(' ');\n }\n\n /**\n * The worktree flavour of the sha/recover token: path, tip, archive tag and the ONE command that\n * puts the directory AND the branch back together.\n *\n * `git worktree add -b <branch> <path> <ref>` is verified by hand and in worktree-reaper.spec.ts —\n * plain `git worktree add <path> <tag>` would restore the files at a DETACHED HEAD, silently losing\n * the branch name the reap destroyed. Falls back to the sha when nothing was archived (retention\n * 'delete'), and to a bare `git worktree add <path>` when there was no branch at all (detached).\n */\n private worktreeDetail(event: BranchMutationEvent): string {\n const ref = event.archiveTag !== '' ? event.archiveTag : event.sha;\n const tokens = [`worktree=${event.worktreePath}`];\n if (event.sha !== '') tokens.push(`sha=${event.sha}`);\n if (event.archiveTag !== '') tokens.push(`archiveTag=${event.archiveTag}`);\n if (ref === '') return tokens.join(' ');\n const recover = event.fromBranch !== ''\n ? `git worktree add -b ${event.fromBranch} ${event.worktreePath} ${ref}`\n : `git worktree add ${event.worktreePath} ${ref}`;\n tokens.push(`recover=${recover}`);\n return tokens.join(' ');\n }\n\n // Collapse newlines/tabs and cap length so one event is always exactly one log line.\n private oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_DETAIL_LEN ? flat : flat.slice(0, MAX_DETAIL_LEN) + '…';\n }\n\n private rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Temporary migration delegators to BranchMutationLog — removed once consumers inject it.\nconst branchMutationLogSvc = new BranchMutationLog();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function branchMutationLogPath(root: string): string {\n return branchMutationLogSvc.branchMutationLogPath(root);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function logBranchMutation(root: string, event: BranchMutationEvent): void {\n branchMutationLogSvc.logBranchMutation(root, event);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"branch-mutation-log.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/branch-mutation-log.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 { DotWebpieces, dotWebpieces } from './state-dir';\nimport { toError } from './to-error';\n\n// The BRANCH-MUTATION log — an audit trail for every workflow verb that RENAMES or MOVES branches.\n// Records START / each phase boundary / END-with-outcome so the next agent (or a human) can\n// reconstruct what the tooling did to the branches. Writes to `.webpieces/logs/branch-mutations.log`\n// (in a linked worktree: `.webpieces/worktrees/<name>/logs/`) — see LOGS_STATE_DIR for why every\n// webpieces log lives under `logs/` and no longer beside the non-log state in `hooks/`.\n// Lives in rules-config (the shared dep of pr-gate) so the pr-gate scripts can call it directly.\n\nconst LOG_FILE = 'branch-mutations.log';\nconst LOG_FILE_PREV = 'branch-mutations.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors the other webpieces logs)\nconst MAX_DETAIL_LEN = 400;\n\n// The workflow verb whose branch mutation is being logged (the bin the AI/human invoked).\n// `auto-reap` is the odd one out: no human invoked it — it is the detached background refresher\n// (sync-main.ts) deleting dead branches on its own. It gets a verb precisely BECAUSE it is\n// unattended: a deletion nobody watched happen is the one that most needs an audit line.\nexport type MutationVerb =\n | 'wp-start-update' | 'wp-finish-update' | 'wp-start-upsert-pr' | 'wp-review-upsert-pr'\n | 'wp-finish-upsert-pr' | 'wp-cleanup' | 'wp-land-pr' | 'auto-reap';\n\n// A boundary within a verb's execution. START/END bracket the whole run; the middle phases mark each\n// irreversible git step so an interrupt leaves a breadcrumb at the last phase reached.\n// REAP is a whole mutation in one line (a branch delete has no phases) — see BranchReaper.\n// REAP_WORKTREE is its worktree twin: archive → `git worktree remove` → `git branch -D`, all three of\n// which succeed or fail as one act. It is a SEPARATE phase, not just a REAP with a path, so that\n// `grep REAP_WORKTREE` answers \"what directories did the tooling delete?\" — a strictly scarier\n// question than \"what refs did it delete?\", since a worktree removal takes real files with it.\nexport type MutationPhase =\n | 'START' | 'BACKUP' | 'CHECKOUT_MAIN' | 'PULL' | 'SQUASH' | 'RENAME'\n | 'FINALIZE' | 'CONFLICT' | 'INTERRUPTED' | 'END' | 'REAP' | 'REAP_WORKTREE';\n\n// Data-only record of one branch-mutation event (per CLAUDE.md: classes for data, explicit construction).\nexport class BranchMutationEvent {\n verb: MutationVerb;\n phase: MutationPhase;\n fromBranch: string = '';\n toBranch: string = '';\n oldMain: string = '';\n newMain: string = '';\n conflict: boolean = false;\n conflictFiles: string[] = [];\n outcome: string = '';\n artifacts: string[] = [];\n // The commit a DELETED branch pointed at, captured immediately before the delete. This is what\n // makes a reap auditable AND reversible: the work is already in main, and the pre-delete tip is\n // still addressable by hash (the reflog holds it ~90 days), so formatDetail renders a literal\n // `recover=git branch <name> <sha>` next to it. Empty for mutations that delete nothing.\n sha: string = '';\n // The `archive/<date>/<branch>` tag written immediately BEFORE a REAP deleted the branch. When set,\n // formatDetail renders `recover=` against the TAG instead of the sha: a tag is a permanent ref that\n // survives `gc` and reflog expiry and can be pushed, whereas a bare sha is only recoverable while\n // this clone's reflog still holds it. Empty when nothing was tagged (retention policy 'delete').\n archiveTag: string = '';\n // The directory a REAP_WORKTREE removed. When set, `recover=` becomes the WORKTREE form\n // (`git worktree add -b <branch> <path> <ref>`) rather than the bare `git branch` form: putting the\n // ref back does not put the directory back, and a recover line that restores half of what was\n // destroyed is worse than none — it reads as done. Empty for every mutation that removes no directory.\n worktreePath: string = '';\n\n constructor(verb: MutationVerb, phase: MutationPhase) {\n this.verb = verb;\n this.phase = phase;\n }\n}\n\n/** Appends branch-mutation audit lines. `@injectable(bindingScopeValues.Singleton)` so it's injectable + drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class BranchMutationLog {\n constructor(private readonly dotDir: DotWebpieces = dotWebpieces) {}\n\n /**\n * LOCAL scope, deliberately — one log per worktree, not one per repo.\n *\n * A SHARED append-only log would genuinely corrupt. `O_APPEND` makes a write indivisible only up to\n * PIPE_BUF, which is 512 bytes on macOS, and a REAP_WORKTREE line carrying\n * `recover=git worktree add -b <branch> <absolute-path> <tag>` exceeds that — concurrent appenders\n * from seven worktrees would interleave into an unrecoverable audit trail, which is the one thing\n * this file exists not to be. Per-worktree, THIS log has one writer and cannot tear.\n *\n * That last claim is narrower than it looks, so do not generalise it (2026-08-06). It holds here\n * because the wp-* bins write this file one command at a time. It does NOT hold for the ai-hook\n * logs in the same directory: Claude Code runs every matching PreToolUse hook IN PARALLEL, and\n * several agents and sessions can share one tree, so a per-worktree name there had several\n * concurrent writers. Those names now carry a `<sessionId>-<agentId|coordinator>-<hook>-` prefix\n * (ai-hook-rules' LogStream). This file keeps a bare name deliberately — LogStream lives in\n * ai-hook-rules, which DEPENDS on rules-config, so the import direction forbids reusing it here.\n * If this log ever gains a second concurrent writer, it needs its own stream identity first.\n *\n * Nothing is lost by keeping it local: under the `worktrees/<name>/` layout the log lives in the\n * PRIMARY clone, so it survives `git worktree remove`, and the whole history is one glob —\n * `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.\n */\n branchMutationLogPath(root: string): string {\n return this.dotDir.logsFile(root, LOG_FILE);\n }\n\n /**\n * Append one tab-separated line per branch-mutation event to\n * `.webpieces/logs/branch-mutations.log`. Swallows all errors — logging must NEVER block or fail\n * the workflow it is observing.\n */\n logBranchMutation(root: string, event: BranchMutationEvent): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n const logsDir = this.dotDir.logs(root);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, LOG_FILE);\n this.rotateLogFile(logPath, path.join(logsDir, LOG_FILE_PREV));\n\n const line = [\n `[${timestamp}]`,\n event.verb,\n event.phase,\n this.oneLine(this.formatDetail(event)),\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // Render only the fields this event actually set, as `key=value` tokens — greppable on one line.\n private formatDetail(event: BranchMutationEvent): string {\n const parts: string[] = [];\n // A rename/move has both ends; a REAP has only the branch it destroyed. Printing `to=?` for\n // the latter reads like a lost destination rather than \"there was never one\".\n if (event.fromBranch !== '' && event.toBranch !== '') parts.push(`from=${event.fromBranch} to=${event.toBranch}`);\n else if (event.fromBranch !== '') parts.push(`branch=${event.fromBranch}`);\n else if (event.toBranch !== '') parts.push(`from=? to=${event.toBranch}`);\n if (event.oldMain !== '' || event.newMain !== '') parts.push(`oldMain=${event.oldMain || '?'} newMain=${event.newMain || '?'}`);\n if (event.conflict) parts.push('conflict=true');\n if (event.conflictFiles.length > 0) parts.push(`conflictFiles=${event.conflictFiles.length}(${event.conflictFiles.join(',')})`);\n if (event.outcome !== '') parts.push(`outcome=${event.outcome}`);\n // Emitted as one unit so the hash is never separated from the command that undoes the delete.\n // Prefer the archive TAG as the recover ref when there is one — it does not expire.\n if (event.worktreePath !== '') {\n parts.push(this.worktreeDetail(event));\n } else if (event.sha !== '' && event.archiveTag !== '') {\n parts.push(\n `sha=${event.sha} archiveTag=${event.archiveTag} ` +\n `recover=git checkout -b ${event.fromBranch || '?'} ${event.archiveTag}`,\n );\n } else if (event.sha !== '') {\n parts.push(`sha=${event.sha} recover=git branch ${event.fromBranch || '?'} ${event.sha}`);\n }\n for (const artifact of event.artifacts) parts.push(`artifact=${artifact}`);\n return parts.join(' ');\n }\n\n /**\n * The worktree flavour of the sha/recover token: path, tip, archive tag and the ONE command that\n * puts the directory AND the branch back together.\n *\n * `git worktree add -b <branch> <path> <ref>` is verified by hand and in worktree-reaper.spec.ts —\n * plain `git worktree add <path> <tag>` would restore the files at a DETACHED HEAD, silently losing\n * the branch name the reap destroyed. Falls back to the sha when nothing was archived (retention\n * 'delete'), and to a bare `git worktree add <path>` when there was no branch at all (detached).\n */\n private worktreeDetail(event: BranchMutationEvent): string {\n const ref = event.archiveTag !== '' ? event.archiveTag : event.sha;\n const tokens = [`worktree=${event.worktreePath}`];\n if (event.sha !== '') tokens.push(`sha=${event.sha}`);\n if (event.archiveTag !== '') tokens.push(`archiveTag=${event.archiveTag}`);\n if (ref === '') return tokens.join(' ');\n const recover = event.fromBranch !== ''\n ? `git worktree add -b ${event.fromBranch} ${event.worktreePath} ${ref}`\n : `git worktree add ${event.worktreePath} ${ref}`;\n tokens.push(`recover=${recover}`);\n return tokens.join(' ');\n }\n\n // Collapse newlines/tabs and cap length so one event is always exactly one log line.\n private oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_DETAIL_LEN ? flat : flat.slice(0, MAX_DETAIL_LEN) + '…';\n }\n\n private rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Temporary migration delegators to BranchMutationLog — removed once consumers inject it.\nconst branchMutationLogSvc = new BranchMutationLog();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function branchMutationLogPath(root: string): string {\n return branchMutationLogSvc.branchMutationLogPath(root);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function logBranchMutation(root: string, event: BranchMutationEvent): void {\n branchMutationLogSvc.logBranchMutation(root, event);\n}\n"]}
|
package/src/state-dir.js
CHANGED
|
@@ -16,7 +16,11 @@ exports.WORKTREE_STATE_DIR = 'worktrees';
|
|
|
16
16
|
// each worktree namespace alike. It used to be `hooks/`, which also holds NON-log state (the dated
|
|
17
17
|
// `hooks/<YYYY-MM-DD>/writeInfo-*.md` rejection details), while the L0 sh shim wrote to `logs/`. Two
|
|
18
18
|
// directories, one of them mixed — so "where are the logs?" had two answers and neither was complete.
|
|
19
|
-
// Every writer
|
|
19
|
+
// Every writer resolves its DIRECTORY through dotWebpieces.logs()/logsFile(), so the layout cannot
|
|
20
|
+
// drift apart again. The FILENAME is a separate question: ai-hook-rules' LogStream prefixes it with
|
|
21
|
+
// <sessionId>-<agentId|coordinator>-<hook>- so that concurrent writers (Claude Code runs all matching
|
|
22
|
+
// PreToolUse hooks IN PARALLEL, and subagents/windows share a tree) never append to one file. This dir
|
|
23
|
+
// is still the one home for all of them.
|
|
20
24
|
exports.LOGS_STATE_DIR = 'logs';
|
|
21
25
|
// Non-log hook state: the dated rejection-detail directories. Named here (rather than re-spelled in
|
|
22
26
|
// each consumer) so the hooks/logs split has exactly one definition.
|
package/src/state-dir.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-dir.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/state-dir.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,+CAAyB;AACzB,mDAA6B;AAC7B,yCAA2D;AAE3D,2CAAgD;AAChD,+DAAyD;AAEzD,uGAAuG;AACvG,2FAA2F;AAC3F,8DAA8D;AACjD,QAAA,kBAAkB,GAAG,WAAW,CAAC;AAE9C,qGAAqG;AACrG,mGAAmG;AACnG,qGAAqG;AACrG,sGAAsG;AACtG,wGAAwG;AAC3F,QAAA,cAAc,GAAG,MAAM,CAAC;AAErC,oGAAoG;AACpG,qEAAqE;AACxD,QAAA,eAAe,GAAG,OAAO,CAAC;AAEvC,oGAAoG;AACpG,oFAAoF;AACpF,MAAM,YAAY,GAAG,MAAM,CAAC;AAE5B;;;;;;GAMG;AACH,MAAa,OAAO;IACP,MAAM,CAAS;IACf,SAAS,CAAS;IAE3B,YAAY,MAAc,EAAE,SAAiB;QACzC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,IAAI,gBAAgB;QAChB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtE,CAAC;CACJ;AAZD,0BAYC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IASQ;IAR7B,kGAAkG;IAClG,mCAAmC;IAClB,aAAa,GAAG,IAAI,GAAG,EAA0B,CAAC;IACnE,2FAA2F;IAC1E,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9C,mGAAmG;IAClF,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IAElD,YAA6B,WAA6B,IAAI,sCAAgB,EAAE;QAAnD,aAAQ,GAAR,QAAQ,CAA2C;IAAG,CAAC;IAEpF;;;;OAIG;IACH,MAAM,CAAC,QAAgB;QACnB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,6BAAiB,CAAC,CAAC;IACpE,CAAC;IAED,8CAA8C;IAC9C,UAAU,CAAC,QAAgB,EAAE,GAAG,QAAkB;QAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;IACzD,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,QAAgB;QAClB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,gBAAgB;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAE1E,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,0BAAkB,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAChG,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACnC,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,wDAAwD;IACxD,SAAS,CAAC,QAAgB,EAAE,GAAG,QAAkB;QAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED;;;;;;;;OAQG;IACH,IAAI,CAAC,QAAgB;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,sBAAc,CAAC,CAAC;QAC/D,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,iGAAiG;IACjG,QAAQ,CAAC,QAAgB,EAAE,GAAG,QAAkB;QAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;IACvD,CAAC;IAED,oFAAoF;IACpF,gBAAgB,CAAC,QAAgB;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAClD,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,QAAgB;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,gBAAgB;YAAE,OAAO,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED,iGAAiG;IACjG,WAAW,CAAC,QAAgB;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,QAAQ,CAAC;QACnC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,YAAY;YAAE,OAAO,QAAQ,CAAC;QACpE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC7C,OAAO,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;IACvD,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,QAAgB;QACtB,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,6BAAiB,CAAC,CAAC;IAClD,CAAC;IAED,kGAAkG;IAClG,mGAAmG;IAC3F,WAAW,CAAC,QAAgB,EAAE,MAAc;QAChD,iGAAiG;QACjG,2FAA2F;QAC3F,gFAAgF;QAChF,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO;QACxC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO;QAC9B,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;IAED,kGAAkG;IAClG,+EAA+E;IACvE,eAAe,CAAC,QAAgB,EAAE,MAAc;QACpD,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO;QAC5C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,uBAAe,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5F,CAAC;IAED,kGAAkG;IAClG,oGAAoG;IACpG,iGAAiG;IACzF,OAAO,CAAC,QAAgB;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QAExC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACpD,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;QAC9D,MAAM,IAAI,GAAG,MAAM,KAAK,IAAI,IAAI,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC3F,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IAChB,CAAC;IAEO,WAAW,CAAC,QAAgB;QAChC,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;IACtD,CAAC;IAED,kGAAkG;IAClG,gFAAgF;IACxE,QAAQ,CAAC,GAAW,EAAE,IAAY;QACtC,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QACtF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACrC,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7C,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAChC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC;CACJ,CAAA;AAxJY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAUE,sCAAgB;GAT9C,YAAY,CAwJxB;AAED,sGAAsG;AACtG,sGAAsG;AACtG,uFAAuF;AAC1E,QAAA,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WEBPIECES_TMP_DIR } from './constants';\nimport { StateDirMigrator } from './state-dir-migration';\n\n// The per-worktree namespace inside the primary clone's `.webpieces/`. A LINKED worktree's local state\n// lives at `<primary>/.webpieces/worktrees/<worktreeName>/`; the primary clone keeps using\n// `<primary>/.webpieces/` directly, exactly as it always has.\nexport const WORKTREE_STATE_DIR = 'worktrees';\n\n// The ONE directory every webpieces `.log` lives in — `<state>/logs/`, for the primary clone and for\n// each worktree namespace alike. It used to be `hooks/`, which also holds NON-log state (the dated\n// `hooks/<YYYY-MM-DD>/writeInfo-*.md` rejection details), while the L0 sh shim wrote to `logs/`. Two\n// directories, one of them mixed — so \"where are the logs?\" had two answers and neither was complete.\n// Every writer now goes through dotWebpieces.logs()/logsFile(), so the layout cannot drift apart again.\nexport const LOGS_STATE_DIR = 'logs';\n\n// Non-log hook state: the dated rejection-detail directories. Named here (rather than re-spelled in\n// each consumer) so the hooks/logs split has exactly one definition.\nexport const HOOKS_STATE_DIR = 'hooks';\n\n// git prints the shared git dir as `<primary>/.git` for a conventional clone. Anything else (a bare\n// repo, `--separate-git-dir`) is a layout we decline to derive a working tree from.\nconst GIT_DIR_NAME = '.git';\n\n/**\n * Data-only carrier for the two paths git is asked for. Per CLAUDE.md: classes for data.\n *\n * `gitDir` is the PER-WORKTREE git dir (`<primary>/.git/worktrees/<name>` in a linked worktree,\n * `<primary>/.git` in the primary clone). `commonDir` is the SHARED one — always `<primary>/.git`.\n * They differ if and only if this is a linked worktree; that is git's own canonical test.\n */\nexport class GitDirs {\n readonly gitDir: string;\n readonly commonDir: string;\n\n constructor(gitDir: string, commonDir: string) {\n this.gitDir = gitDir;\n this.commonDir = commonDir;\n }\n\n get isLinkedWorktree(): boolean {\n return path.resolve(this.gitDir) !== path.resolve(this.commonDir);\n }\n}\n\n/**\n * WHERE a piece of `.webpieces/` state belongs — the ONE resolver every reader and writer must go\n * through, with TWO named methods because there are exactly two answers and a call site must DECLARE\n * which one it means.\n *\n * dotWebpieces.shared(dir) → <primary>/.webpieces (repo-wide facts)\n * dotWebpieces.local(dir) → <primary>/.webpieces/worktrees/<name> (this worktree only)\n * → <primary>/.webpieces (…in the primary clone)\n *\n * ─── The bug ───────────────────────────────────────────────────────────────────────────────────────\n * `.webpieces/` is gitignored, and it was anchored at the directory holding webpieces.config.json —\n * which in a linked worktree is the WORKTREE. So a repo with seven worktrees had SEVEN independent\n * copies of files that describe the WHOLE REPO. `merged-branches.json` holds verdicts for every branch\n * AND every worktree in the repo; N copies is N divergent truths, and the guards read them as fact.\n * Observed in the field: branch-creation-guard asserted \"8 parked local branches\" while\n * `git branch --list` showed ONE — it was reading a cache written before deletions performed from a\n * DIFFERENT worktree — and it then blocked a legitimate `git worktree add` on that fiction.\n *\n * ─── Why two explicit methods and not a symlink ────────────────────────────────────────────────────\n * A `<worktree>/.webpieces` → `<primary>/…/worktrees/<name>` symlink would have left every call site\n * untouched, which is seductive and wrong. Nothing hooks `git worktree add`, so the link needs LAZY\n * creation that also has to handle \"link already exists\", \"link points somewhere else\", and \"a real\n * directory is already there\" — invisible filesystem magic with a Windows failure mode. Worse, the safe\n * way to write a shared file under concurrency is temp-file-then-`rename()`, and `rename(2)` acts on\n * the PATH, not the link: it REPLACES a symlink with a real file. That silently works for one writer\n * and diverges for everyone else — the exact bug being fixed, but invisible. An explicit call is\n * greppable, testable, and forces each site to say which scope it means.\n *\n * ─── Which scope is which (the scope assignment is deliberate, not incidental) ─────────────────────\n * shared():\n * • merged-branches.json — verdicts for every branch and worktree in the repo. Atomically written.\n * • main-sync-status.json AND main-sync.lock.json — there is one `main` and one `.git`, so there is\n * one refresher. A lock inside a per-worktree directory locks nothing.\n * local():\n * • logs/*.log, INCLUDING branch-mutations.log. A shared append-only log genuinely corrupts:\n * `O_APPEND` writes are indivisible only under PIPE_BUF, which is 512 bytes on macOS, and a\n * `recover=git worktree add -b <branch> <abs-path> <tag>` line with real paths exceeds that. A\n * per-worktree log has exactly ONE writer and cannot tear, and under this layout it already\n * survives the worktree's deletion — recovery is one glob over\n * `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.\n * • merge-info/staged|merged/<branch>/ and its index.json, pr-review/<branch>/, instruct-ai/, and\n * every other per-tree scratch file.\n *\n * ─── The boundary that must not be blurred ─────────────────────────────────────────────────────────\n * ONLY the gitignored `.webpieces/` STATE relocates. `webpieces.config.json` is TRACKED IN GIT and is\n * therefore part of the BRANCH: a branch may legitimately change its own rules and that must keep\n * working. Config resolution is untouched — still per-worktree, via findConfigFile /\n * RepoRootFinder.resolveRepoRoot. Nothing in this class reads or moves config.\n *\n * ─── Why `--git-dir` / `--git-common-dir`, and not one of the existing services ────────────────────\n * They are git's own answers, from any subdirectory, in one cheap local call, with no `.git`-file\n * parsing by hand (which gets `--separate-git-dir` and submodules wrong). Two existing mechanisms do\n * distinguish primary from linked, and neither is the right authority here:\n * • `WorktreeService` answers \"what worktrees exist and what do they hold\" — a repo-wide ENUMERATION\n * (`git worktree list --porcelain`) for the caps and the reaper. Using it for a path lookup would\n * run an enumeration on the hook's blocking path for every state access, and it fails SOFT to `[]`,\n * which here would read as \"there is no primary clone\" on exactly the degraded repo where the\n * answer matters most. It also does not expose the worktree's git NAME, which is the namespace key.\n * • `EffectiveTreeResolver` (#524) answers \"which tree does this COMMAND act on\" — it takes a command\n * string and is a policy input to the bash guards, not a filesystem-path resolver.\n * Both remain authoritative for their own questions. This asks the narrowest one — two path strings —\n * and lives in rules-config, UNDER both, which is where a primitive that pr-gate, ai-hook-rules and\n * code-rules all need has to sit.\n *\n * Fails CLOSED to the pre-change behaviour: when git cannot answer, every path here collapses to\n * `<startDir-root>/.webpieces` exactly as before. Degrading to merely-suboptimal beats throwing on a\n * hook's blocking path.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DotWebpieces {\n // treeRoot → git's answer. One `git rev-parse` pair per root per process; every later path lookup\n // in that invocation is a Map hit.\n private readonly gitDirsByRoot = new Map<string, GitDirs | null>();\n // Roots whose legacy per-worktree `.webpieces/` has already been considered for migration.\n private readonly migrated = new Set<string>();\n // Roots whose legacy `hooks/*.log` files have already been considered for relocation into `logs/`.\n private readonly logsMigrated = new Set<string>();\n\n constructor(private readonly migrator: StateDirMigrator = new StateDirMigrator()) {}\n\n /**\n * REPO-WIDE state: `<primary>/.webpieces`. Use ONLY for facts about the repo rather than about one\n * worktree — today that is merged-branches.json and the main-sync status + lock. Identical from\n * every worktree, and never behind an indirection, so an atomic `rename()` into it is safe.\n */\n shared(startDir: string): string {\n return path.join(this.primaryRoot(startDir), WEBPIECES_TMP_DIR);\n }\n\n /** A path beneath the repo-wide state dir. */\n sharedFile(startDir: string, ...segments: string[]): string {\n return path.join(this.shared(startDir), ...segments);\n }\n\n /**\n * THIS WORKTREE's private state: `<primary>/.webpieces/worktrees/<name>` for a linked worktree, and\n * `<primary>/.webpieces` for the primary clone, which keeps its state exactly where it has always\n * been. Fully isolated — two worktrees never write the same path, so nothing here needs a lock.\n *\n * The first call for a linked worktree also MIGRATES a legacy real `<worktree>/.webpieces/`\n * directory into the namespace, so in-flight merge / pr-review state written under the old scheme\n * (or by an older PUBLISHED build during the transition) is picked up rather than orphaned.\n */\n local(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return this.shared(startDir);\n\n const target = path.join(this.shared(startDir), WORKTREE_STATE_DIR, path.basename(dirs.gitDir));\n this.migrateOnce(startDir, target);\n return target;\n }\n\n /** A path beneath this worktree's private state dir. */\n localFile(startDir: string, ...segments: string[]): string {\n return path.join(this.local(startDir), ...segments);\n }\n\n /**\n * THE log directory — `<local()>/logs` — and the only place a webpieces `.log` may be written, in\n * the primary clone and in every worktree namespace alike.\n *\n * The first call per tree also RELOCATES any `hooks/*.log` written by an older release (or by the\n * still-published build during the transition window) into `logs/`, so upgrading does not orphan\n * the history a human is mid-way through reading. Same safety rule as StateDirMigrator.migrate:\n * an occupied destination is never overwritten — the old copy is left where it is.\n */\n logs(startDir: string): string {\n const target = path.join(this.local(startDir), LOGS_STATE_DIR);\n this.migrateLogsOnce(startDir, target);\n return target;\n }\n\n /** A path beneath the log directory — `dotWebpieces.logsFile(root, 'guard-invocations.log')`. */\n logsFile(startDir: string, ...segments: string[]): string {\n return path.join(this.logs(startDir), ...segments);\n }\n\n /** True when `startDir` sits in a LINKED worktree rather than the primary clone. */\n isLinkedWorktree(startDir: string): boolean {\n const dirs = this.gitDirs(startDir);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * git's own name for this linked worktree — the basename of `<primary>/.git/worktrees/<name>`, and\n * the namespace key under `worktrees/`. Empty for the primary clone. git's name rather than the\n * directory's basename, so two worktrees checked out into same-named directories under different\n * parents cannot collide.\n */\n worktreeName(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return '';\n return path.basename(dirs.gitDir);\n }\n\n /** The primary clone's root, from any worktree. Falls back to git's toplevel-less best guess. */\n primaryRoot(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null) return startDir;\n if (path.basename(dirs.commonDir) !== GIT_DIR_NAME) return startDir;\n const primary = path.dirname(dirs.commonDir);\n return fs.existsSync(primary) ? primary : startDir;\n }\n\n /**\n * The PRE-change location, `<treeRoot>/.webpieces` — what every call site used to compute. Public\n * because the migrator and its specs must be able to name the thing being migrated FROM, and\n * because the transition-window fallback readers need it.\n */\n legacyDir(treeRoot: string): string {\n return path.join(treeRoot, WEBPIECES_TMP_DIR);\n }\n\n // Drain a legacy per-worktree `.webpieces/` into this worktree's namespace, at most once per tree\n // per process. Migration is idempotent, but it touches the filesystem on the hook's blocking path.\n private migrateOnce(startDir: string, target: string): void {\n // Guard on the CHEAP key first. `local()` is called many times per invocation, and resolving the\n // worktree toplevel costs a `git rev-parse` — doing that before the once-check would put a\n // process spawn on the hook's blocking path for every single state-path lookup.\n if (this.migrated.has(startDir)) return;\n this.migrated.add(startDir);\n const toplevel = this.gitToplevel(startDir);\n if (toplevel === null) return;\n this.migrator.migrate(this.legacyDir(toplevel), target);\n }\n\n // Drain `hooks/*.log` into `logs/`, at most once per tree per process. Idempotent, but it touches\n // the filesystem on the hook's blocking path, so it must not run per log line.\n private migrateLogsOnce(startDir: string, target: string): void {\n if (this.logsMigrated.has(startDir)) return;\n this.logsMigrated.add(startDir);\n this.migrator.migrateLogFiles(path.join(this.local(startDir), HOOKS_STATE_DIR), target);\n }\n\n // Both git dirs for `startDir`, cached, or null when this is not a git repo / git is unavailable.\n // `status !== 0` IS the expected \"not a repo\" answer (spawnSync does not throw on a non-zero exit),\n // so there is no try/catch here swallowing a real git crash. Mirrors RepoRootFinder.gitToplevel.\n private gitDirs(startDir: string): GitDirs | null {\n const cached = this.gitDirsByRoot.get(startDir);\n if (cached !== undefined) return cached;\n\n const gitDir = this.revParse(startDir, '--git-dir');\n const commonDir = this.revParse(startDir, '--git-common-dir');\n const dirs = gitDir === null || commonDir === null ? null : new GitDirs(gitDir, commonDir);\n this.gitDirsByRoot.set(startDir, dirs);\n return dirs;\n }\n\n private gitToplevel(startDir: string): string | null {\n return this.revParse(startDir, '--show-toplevel');\n }\n\n // One `git rev-parse <flag>`, resolved to an absolute path (git prints a bare `.git`, relative to\n // the tree, in the primary clone, and an absolute path from a linked worktree).\n private revParse(cwd: string, flag: string): string | null {\n const result = spawnSync('git', ['-C', cwd, 'rev-parse', flag], { encoding: 'utf8' });\n if (result.status !== 0) return null;\n const printed = (result.stdout ?? '').trim();\n if (printed === '') return null;\n return path.resolve(cwd, printed);\n }\n}\n\n// Process-wide instance for the many non-DI call sites (hooks, detached refreshers, wp-* bins, eslint\n// rules). Sharing one instance is what makes the git-resolution cache and the once-per-tree migration\n// worth having; inversify still injects the singleton wherever a container is in play.\nexport const dotWebpieces = new DotWebpieces();\n"]}
|
|
1
|
+
{"version":3,"file":"state-dir.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/state-dir.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WEBPIECES_TMP_DIR } from './constants';\nimport { StateDirMigrator } from './state-dir-migration';\n\n// The per-worktree namespace inside the primary clone's `.webpieces/`. A LINKED worktree's local state\n// lives at `<primary>/.webpieces/worktrees/<worktreeName>/`; the primary clone keeps using\n// `<primary>/.webpieces/` directly, exactly as it always has.\nexport const WORKTREE_STATE_DIR = 'worktrees';\n\n// The ONE directory every webpieces `.log` lives in — `<state>/logs/`, for the primary clone and for\n// each worktree namespace alike. It used to be `hooks/`, which also holds NON-log state (the dated\n// `hooks/<YYYY-MM-DD>/writeInfo-*.md` rejection details), while the L0 sh shim wrote to `logs/`. Two\n// directories, one of them mixed — so \"where are the logs?\" had two answers and neither was complete.\n// Every writer resolves its DIRECTORY through dotWebpieces.logs()/logsFile(), so the layout cannot\n// drift apart again. The FILENAME is a separate question: ai-hook-rules' LogStream prefixes it with\n// <sessionId>-<agentId|coordinator>-<hook>- so that concurrent writers (Claude Code runs all matching\n// PreToolUse hooks IN PARALLEL, and subagents/windows share a tree) never append to one file. This dir\n// is still the one home for all of them.\nexport const LOGS_STATE_DIR = 'logs';\n\n// Non-log hook state: the dated rejection-detail directories. Named here (rather than re-spelled in\n// each consumer) so the hooks/logs split has exactly one definition.\nexport const HOOKS_STATE_DIR = 'hooks';\n\n// git prints the shared git dir as `<primary>/.git` for a conventional clone. Anything else (a bare\n// repo, `--separate-git-dir`) is a layout we decline to derive a working tree from.\nconst GIT_DIR_NAME = '.git';\n\n/**\n * Data-only carrier for the two paths git is asked for. Per CLAUDE.md: classes for data.\n *\n * `gitDir` is the PER-WORKTREE git dir (`<primary>/.git/worktrees/<name>` in a linked worktree,\n * `<primary>/.git` in the primary clone). `commonDir` is the SHARED one — always `<primary>/.git`.\n * They differ if and only if this is a linked worktree; that is git's own canonical test.\n */\nexport class GitDirs {\n readonly gitDir: string;\n readonly commonDir: string;\n\n constructor(gitDir: string, commonDir: string) {\n this.gitDir = gitDir;\n this.commonDir = commonDir;\n }\n\n get isLinkedWorktree(): boolean {\n return path.resolve(this.gitDir) !== path.resolve(this.commonDir);\n }\n}\n\n/**\n * WHERE a piece of `.webpieces/` state belongs — the ONE resolver every reader and writer must go\n * through, with TWO named methods because there are exactly two answers and a call site must DECLARE\n * which one it means.\n *\n * dotWebpieces.shared(dir) → <primary>/.webpieces (repo-wide facts)\n * dotWebpieces.local(dir) → <primary>/.webpieces/worktrees/<name> (this worktree only)\n * → <primary>/.webpieces (…in the primary clone)\n *\n * ─── The bug ───────────────────────────────────────────────────────────────────────────────────────\n * `.webpieces/` is gitignored, and it was anchored at the directory holding webpieces.config.json —\n * which in a linked worktree is the WORKTREE. So a repo with seven worktrees had SEVEN independent\n * copies of files that describe the WHOLE REPO. `merged-branches.json` holds verdicts for every branch\n * AND every worktree in the repo; N copies is N divergent truths, and the guards read them as fact.\n * Observed in the field: branch-creation-guard asserted \"8 parked local branches\" while\n * `git branch --list` showed ONE — it was reading a cache written before deletions performed from a\n * DIFFERENT worktree — and it then blocked a legitimate `git worktree add` on that fiction.\n *\n * ─── Why two explicit methods and not a symlink ────────────────────────────────────────────────────\n * A `<worktree>/.webpieces` → `<primary>/…/worktrees/<name>` symlink would have left every call site\n * untouched, which is seductive and wrong. Nothing hooks `git worktree add`, so the link needs LAZY\n * creation that also has to handle \"link already exists\", \"link points somewhere else\", and \"a real\n * directory is already there\" — invisible filesystem magic with a Windows failure mode. Worse, the safe\n * way to write a shared file under concurrency is temp-file-then-`rename()`, and `rename(2)` acts on\n * the PATH, not the link: it REPLACES a symlink with a real file. That silently works for one writer\n * and diverges for everyone else — the exact bug being fixed, but invisible. An explicit call is\n * greppable, testable, and forces each site to say which scope it means.\n *\n * ─── Which scope is which (the scope assignment is deliberate, not incidental) ─────────────────────\n * shared():\n * • merged-branches.json — verdicts for every branch and worktree in the repo. Atomically written.\n * • main-sync-status.json AND main-sync.lock.json — there is one `main` and one `.git`, so there is\n * one refresher. A lock inside a per-worktree directory locks nothing.\n * local():\n * • logs/*.log, INCLUDING branch-mutations.log. A shared append-only log genuinely corrupts:\n * `O_APPEND` writes are indivisible only under PIPE_BUF, which is 512 bytes on macOS, and a\n * `recover=git worktree add -b <branch> <abs-path> <tag>` line with real paths exceeds that. A\n * per-worktree log has exactly ONE writer and cannot tear, and under this layout it already\n * survives the worktree's deletion — recovery is one glob over\n * `<primary>/.webpieces/worktrees/*/logs/branch-mutations.log`.\n * • merge-info/staged|merged/<branch>/ and its index.json, pr-review/<branch>/, instruct-ai/, and\n * every other per-tree scratch file.\n *\n * ─── The boundary that must not be blurred ─────────────────────────────────────────────────────────\n * ONLY the gitignored `.webpieces/` STATE relocates. `webpieces.config.json` is TRACKED IN GIT and is\n * therefore part of the BRANCH: a branch may legitimately change its own rules and that must keep\n * working. Config resolution is untouched — still per-worktree, via findConfigFile /\n * RepoRootFinder.resolveRepoRoot. Nothing in this class reads or moves config.\n *\n * ─── Why `--git-dir` / `--git-common-dir`, and not one of the existing services ────────────────────\n * They are git's own answers, from any subdirectory, in one cheap local call, with no `.git`-file\n * parsing by hand (which gets `--separate-git-dir` and submodules wrong). Two existing mechanisms do\n * distinguish primary from linked, and neither is the right authority here:\n * • `WorktreeService` answers \"what worktrees exist and what do they hold\" — a repo-wide ENUMERATION\n * (`git worktree list --porcelain`) for the caps and the reaper. Using it for a path lookup would\n * run an enumeration on the hook's blocking path for every state access, and it fails SOFT to `[]`,\n * which here would read as \"there is no primary clone\" on exactly the degraded repo where the\n * answer matters most. It also does not expose the worktree's git NAME, which is the namespace key.\n * • `EffectiveTreeResolver` (#524) answers \"which tree does this COMMAND act on\" — it takes a command\n * string and is a policy input to the bash guards, not a filesystem-path resolver.\n * Both remain authoritative for their own questions. This asks the narrowest one — two path strings —\n * and lives in rules-config, UNDER both, which is where a primitive that pr-gate, ai-hook-rules and\n * code-rules all need has to sit.\n *\n * Fails CLOSED to the pre-change behaviour: when git cannot answer, every path here collapses to\n * `<startDir-root>/.webpieces` exactly as before. Degrading to merely-suboptimal beats throwing on a\n * hook's blocking path.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DotWebpieces {\n // treeRoot → git's answer. One `git rev-parse` pair per root per process; every later path lookup\n // in that invocation is a Map hit.\n private readonly gitDirsByRoot = new Map<string, GitDirs | null>();\n // Roots whose legacy per-worktree `.webpieces/` has already been considered for migration.\n private readonly migrated = new Set<string>();\n // Roots whose legacy `hooks/*.log` files have already been considered for relocation into `logs/`.\n private readonly logsMigrated = new Set<string>();\n\n constructor(private readonly migrator: StateDirMigrator = new StateDirMigrator()) {}\n\n /**\n * REPO-WIDE state: `<primary>/.webpieces`. Use ONLY for facts about the repo rather than about one\n * worktree — today that is merged-branches.json and the main-sync status + lock. Identical from\n * every worktree, and never behind an indirection, so an atomic `rename()` into it is safe.\n */\n shared(startDir: string): string {\n return path.join(this.primaryRoot(startDir), WEBPIECES_TMP_DIR);\n }\n\n /** A path beneath the repo-wide state dir. */\n sharedFile(startDir: string, ...segments: string[]): string {\n return path.join(this.shared(startDir), ...segments);\n }\n\n /**\n * THIS WORKTREE's private state: `<primary>/.webpieces/worktrees/<name>` for a linked worktree, and\n * `<primary>/.webpieces` for the primary clone, which keeps its state exactly where it has always\n * been. Fully isolated — two worktrees never write the same path, so nothing here needs a lock.\n *\n * The first call for a linked worktree also MIGRATES a legacy real `<worktree>/.webpieces/`\n * directory into the namespace, so in-flight merge / pr-review state written under the old scheme\n * (or by an older PUBLISHED build during the transition) is picked up rather than orphaned.\n */\n local(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return this.shared(startDir);\n\n const target = path.join(this.shared(startDir), WORKTREE_STATE_DIR, path.basename(dirs.gitDir));\n this.migrateOnce(startDir, target);\n return target;\n }\n\n /** A path beneath this worktree's private state dir. */\n localFile(startDir: string, ...segments: string[]): string {\n return path.join(this.local(startDir), ...segments);\n }\n\n /**\n * THE log directory — `<local()>/logs` — and the only place a webpieces `.log` may be written, in\n * the primary clone and in every worktree namespace alike.\n *\n * The first call per tree also RELOCATES any `hooks/*.log` written by an older release (or by the\n * still-published build during the transition window) into `logs/`, so upgrading does not orphan\n * the history a human is mid-way through reading. Same safety rule as StateDirMigrator.migrate:\n * an occupied destination is never overwritten — the old copy is left where it is.\n */\n logs(startDir: string): string {\n const target = path.join(this.local(startDir), LOGS_STATE_DIR);\n this.migrateLogsOnce(startDir, target);\n return target;\n }\n\n /** A path beneath the log directory — `dotWebpieces.logsFile(root, 'guard-invocations.log')`. */\n logsFile(startDir: string, ...segments: string[]): string {\n return path.join(this.logs(startDir), ...segments);\n }\n\n /** True when `startDir` sits in a LINKED worktree rather than the primary clone. */\n isLinkedWorktree(startDir: string): boolean {\n const dirs = this.gitDirs(startDir);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * git's own name for this linked worktree — the basename of `<primary>/.git/worktrees/<name>`, and\n * the namespace key under `worktrees/`. Empty for the primary clone. git's name rather than the\n * directory's basename, so two worktrees checked out into same-named directories under different\n * parents cannot collide.\n */\n worktreeName(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null || !dirs.isLinkedWorktree) return '';\n return path.basename(dirs.gitDir);\n }\n\n /** The primary clone's root, from any worktree. Falls back to git's toplevel-less best guess. */\n primaryRoot(startDir: string): string {\n const dirs = this.gitDirs(startDir);\n if (dirs === null) return startDir;\n if (path.basename(dirs.commonDir) !== GIT_DIR_NAME) return startDir;\n const primary = path.dirname(dirs.commonDir);\n return fs.existsSync(primary) ? primary : startDir;\n }\n\n /**\n * The PRE-change location, `<treeRoot>/.webpieces` — what every call site used to compute. Public\n * because the migrator and its specs must be able to name the thing being migrated FROM, and\n * because the transition-window fallback readers need it.\n */\n legacyDir(treeRoot: string): string {\n return path.join(treeRoot, WEBPIECES_TMP_DIR);\n }\n\n // Drain a legacy per-worktree `.webpieces/` into this worktree's namespace, at most once per tree\n // per process. Migration is idempotent, but it touches the filesystem on the hook's blocking path.\n private migrateOnce(startDir: string, target: string): void {\n // Guard on the CHEAP key first. `local()` is called many times per invocation, and resolving the\n // worktree toplevel costs a `git rev-parse` — doing that before the once-check would put a\n // process spawn on the hook's blocking path for every single state-path lookup.\n if (this.migrated.has(startDir)) return;\n this.migrated.add(startDir);\n const toplevel = this.gitToplevel(startDir);\n if (toplevel === null) return;\n this.migrator.migrate(this.legacyDir(toplevel), target);\n }\n\n // Drain `hooks/*.log` into `logs/`, at most once per tree per process. Idempotent, but it touches\n // the filesystem on the hook's blocking path, so it must not run per log line.\n private migrateLogsOnce(startDir: string, target: string): void {\n if (this.logsMigrated.has(startDir)) return;\n this.logsMigrated.add(startDir);\n this.migrator.migrateLogFiles(path.join(this.local(startDir), HOOKS_STATE_DIR), target);\n }\n\n // Both git dirs for `startDir`, cached, or null when this is not a git repo / git is unavailable.\n // `status !== 0` IS the expected \"not a repo\" answer (spawnSync does not throw on a non-zero exit),\n // so there is no try/catch here swallowing a real git crash. Mirrors RepoRootFinder.gitToplevel.\n private gitDirs(startDir: string): GitDirs | null {\n const cached = this.gitDirsByRoot.get(startDir);\n if (cached !== undefined) return cached;\n\n const gitDir = this.revParse(startDir, '--git-dir');\n const commonDir = this.revParse(startDir, '--git-common-dir');\n const dirs = gitDir === null || commonDir === null ? null : new GitDirs(gitDir, commonDir);\n this.gitDirsByRoot.set(startDir, dirs);\n return dirs;\n }\n\n private gitToplevel(startDir: string): string | null {\n return this.revParse(startDir, '--show-toplevel');\n }\n\n // One `git rev-parse <flag>`, resolved to an absolute path (git prints a bare `.git`, relative to\n // the tree, in the primary clone, and an absolute path from a linked worktree).\n private revParse(cwd: string, flag: string): string | null {\n const result = spawnSync('git', ['-C', cwd, 'rev-parse', flag], { encoding: 'utf8' });\n if (result.status !== 0) return null;\n const printed = (result.stdout ?? '').trim();\n if (printed === '') return null;\n return path.resolve(cwd, printed);\n }\n}\n\n// Process-wide instance for the many non-DI call sites (hooks, detached refreshers, wp-* bins, eslint\n// rules). Sharing one instance is what makes the git-resolution cache and the once-per-tree migration\n// worth having; inversify still injects the singleton wherever a container is in play.\nexport const dotWebpieces = new DotWebpieces();\n"]}
|