@webpieces/rules-config 0.4.786 → 0.4.788
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/aged-tree-sweep.d.ts +4 -1
- package/src/aged-tree-sweep.js +4 -1
- package/src/aged-tree-sweep.js.map +1 -1
- 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/harness-agent-activity.d.ts +5 -3
- package/src/harness-agent-activity.js +15 -14
- package/src/harness-agent-activity.js.map +1 -1
- package/src/index.d.ts +4 -0
- package/src/index.js +18 -4
- package/src/index.js.map +1 -1
- package/src/repo-scratch-dirs.d.ts +20 -0
- package/src/repo-scratch-dirs.js +54 -0
- package/src/repo-scratch-dirs.js.map +1 -0
- package/src/review-provenance.d.ts +22 -10
- package/src/review-provenance.js +36 -22
- package/src/review-provenance.js.map +1 -1
- package/src/spec-temp-dirs.d.ts +37 -0
- package/src/spec-temp-dirs.js +112 -0
- package/src/spec-temp-dirs.js.map +1 -0
- package/src/subagent-provenance.d.ts +49 -10
- package/src/subagent-provenance.js +67 -31
- package/src/subagent-provenance.js.map +1 -1
- package/src/tmp-scratch-sweep.d.ts +32 -0
- package/src/tmp-scratch-sweep.js +99 -0
- package/src/tmp-scratch-sweep.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.788",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/aged-tree-sweep.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How long a file survives an AGED-TREE SWEEP without being rewritten — the policy `cleanTmp` applies
|
|
3
|
-
* to `{repo}/.webpieces
|
|
3
|
+
* to `{repo}/.webpieces`.
|
|
4
|
+
*
|
|
5
|
+
* It is NOT the only root webpieces writes under, though this comment used to say so: scratch trees go
|
|
6
|
+
* to `os.tmpdir()` as well, on a much shorter cutoff and through `TmpScratchSweeper`. See that file.
|
|
4
7
|
*
|
|
5
8
|
* NOT the same knob as `DEFAULT_RETENTION_DAYS` in review-provenance.ts, which happens to be 30 as well.
|
|
6
9
|
* That one is a CONFIGURABLE ceiling on reviewer transcripts, overridable per repo in settings; this one
|
package/src/aged-tree-sweep.js
CHANGED
|
@@ -7,7 +7,10 @@ const path = tslib_1.__importStar(require("path"));
|
|
|
7
7
|
const inversify_1 = require("inversify");
|
|
8
8
|
/**
|
|
9
9
|
* How long a file survives an AGED-TREE SWEEP without being rewritten — the policy `cleanTmp` applies
|
|
10
|
-
* to `{repo}/.webpieces
|
|
10
|
+
* to `{repo}/.webpieces`.
|
|
11
|
+
*
|
|
12
|
+
* It is NOT the only root webpieces writes under, though this comment used to say so: scratch trees go
|
|
13
|
+
* to `os.tmpdir()` as well, on a much shorter cutoff and through `TmpScratchSweeper`. See that file.
|
|
11
14
|
*
|
|
12
15
|
* NOT the same knob as `DEFAULT_RETENTION_DAYS` in review-provenance.ts, which happens to be 30 as well.
|
|
13
16
|
* That one is a CONFIGURABLE ceiling on reviewer transcripts, overridable per repo in settings; this one
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aged-tree-sweep.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/aged-tree-sweep.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,yCAA2D;AAE3D
|
|
1
|
+
{"version":3,"file":"aged-tree-sweep.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/aged-tree-sweep.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,yCAA2D;AAE3D;;;;;;;;;;;GAWG;AACU,QAAA,cAAc,GAAG,EAAE,CAAC;AAEjC,wGAAwG;AACxG,MAAa,UAAU;IACnB,KAAK,CAAS;IACd,IAAI,CAAS;IAEb,YAAY,KAAK,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,KAAiB;QACjB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;QAC1B,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,IAAI,KAAK;QACL,OAAO,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC;IAC/C,CAAC;CACJ;AAjBD,gCAiBC;AAED;;;;;;;;;;;GAWG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;;;;OAMG;IACH,KAAK,CAAC,IAAY,EAAE,QAAgB,EAAE,WAAsD,GAAS,EAAE,GAAE,CAAC;QACtG,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,UAAU,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IAED,mGAAmG;IACnG,wDAAwD;IACxD,yDAAyD;IACjD,IAAI,CAAC,GAAW,EAAE,GAAW,EAAE,QAAgB,EAAE,MAAe,EAAE,QAAmD;QACzH,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACnC,6FAA6F;YAC7F,wFAAwF;YACxF,MAAM,IAAI,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACrB,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC/D,CAAC;iBAAM,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,QAAQ,EAAE,CAAC;gBACxC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACjC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBACtB,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;YACrB,CAAC;QACL,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9C,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YAClB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACpB,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;QACpB,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ,CAAA;AAtCY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAsC3B","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * How long a file survives an AGED-TREE SWEEP without being rewritten — the policy `cleanTmp` applies\n * to `{repo}/.webpieces`.\n *\n * It is NOT the only root webpieces writes under, though this comment used to say so: scratch trees go\n * to `os.tmpdir()` as well, on a much shorter cutoff and through `TmpScratchSweeper`. See that file.\n *\n * NOT the same knob as `DEFAULT_RETENTION_DAYS` in review-provenance.ts, which happens to be 30 as well.\n * That one is a CONFIGURABLE ceiling on reviewer transcripts, overridable per repo in settings; this one\n * is the fixed sweep cutoff and nothing reads it from config. Same number today, different decisions,\n * so folding them would couple a repo's transcript-retention preference to when scratch dirs are reaped.\n */\nexport const RETENTION_DAYS = 30;\n\n/** What one sweep reaped: files deleted for age, and directories pruned because they ended up empty. */\nexport class SweepCount {\n files: number;\n dirs: number;\n\n constructor(files = 0, dirs = 0) {\n this.files = files;\n this.dirs = dirs;\n }\n\n add(other: SweepCount): void {\n this.files += other.files;\n this.dirs += other.dirs;\n }\n\n get empty(): boolean {\n return this.files === 0 && this.dirs === 0;\n }\n}\n\n/**\n * The 30-day depth-first reap, as ONE implementation.\n *\n * It was written inside `CleanTmp` for `<primary>/.webpieces`, and extracted here when a second root\n * (the machine-global PR-body store) needed the identical policy. That second root is gone — the gated\n * merge body is the PR's own description now, see `decisions/0005` — so today there is one caller. The\n * loop stays extracted anyway: it is the sweep policy, testable on its own root, and `CleanTmp` reads\n * as the command it is rather than as a deletion algorithm.\n *\n * `onDelete` is a reporter, not a policy hook: it is told what was removed AFTER the decision, so a\n * caller can print a line and cannot change the outcome.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class AgedTreeSweeper {\n /**\n * Delete every file under `root` older than `cutoffMs`, then prune every directory left empty.\n * `root` itself is swept INTO but never removed — every writer `mkdirSync`s its target, so an empty\n * root is harmless, whereas removing it can race a concurrent writer.\n *\n * A missing `root` is a no-op, not an error.\n */\n sweep(root: string, cutoffMs: number, onDelete: (removed: string, isDir: boolean) => void = (): void => {}): SweepCount {\n if (!fs.existsSync(root)) return new SweepCount();\n return this.walk(root, Date.now(), cutoffMs, true, onDelete);\n }\n\n // Depth-first, POST-order: children first, so a directory whose last file just aged out is seen as\n // empty on the way back up and pruned in the same pass.\n // eslint-disable-next-line @typescript-eslint/max-params\n private walk(dir: string, now: number, cutoffMs: number, isRoot: boolean, onDelete: (removed: string, isDir: boolean) => void): SweepCount {\n const count = new SweepCount();\n for (const entry of fs.readdirSync(dir)) {\n const full = path.join(dir, entry);\n // lstat, never stat: a symlink is a leaf that ages out like a file and is NEVER followed, so\n // a sweep can never wander outside `dir` or delete somebody else's file through a link.\n const stat = fs.lstatSync(full);\n if (stat.isDirectory()) {\n count.add(this.walk(full, now, cutoffMs, false, onDelete));\n } else if (now - stat.mtimeMs >= cutoffMs) {\n fs.rmSync(full, { force: true });\n onDelete(full, false);\n count.files += 1;\n }\n }\n if (!isRoot && fs.readdirSync(dir).length === 0) {\n fs.rmdirSync(dir);\n onDelete(dir, true);\n count.dirs += 1;\n }\n return count;\n }\n}\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ONE PLACE THAT KNOWS WHERE CLAUDE CODE KEEPS ITS ON-DISK STATE.
|
|
3
|
+
*
|
|
4
|
+
* ─── WHY THIS FILE EXISTS ────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* `~/.claude` is a DEFAULT, not the location. Claude Code relocates the whole tree when
|
|
6
|
+
* `$CLAUDE_CONFIG_DIR` is set, and three modules in this package read that tree: reviewer provenance
|
|
7
|
+
* (`subagent-provenance.ts`), the audit record (`review-provenance.ts`) and the worktree-reap veto
|
|
8
|
+
* (`harness-agent-activity.ts`). Only the third one resolved it correctly; the other two joined
|
|
9
|
+
* `os.homedir()` with a hardcoded `'.claude'`.
|
|
10
|
+
*
|
|
11
|
+
* The consequence was not a degraded answer, it was a HARD BLOCK with no user-side workaround: on a
|
|
12
|
+
* machine with `CLAUDE_CONFIG_DIR=~/.claude-work`, provenance found zero transcripts — not the
|
|
13
|
+
* reviewers', not even the main agent's — so `wp-finish-upsert-pr` refused every PR whose reviewers
|
|
14
|
+
* had genuinely run and genuinely passed, and the refusal blamed the reviewers' cwd. The audit record
|
|
15
|
+
* it wrote said `mainTranscript: ""`, which is the field that settles it: the main transcript is found
|
|
16
|
+
* by session id alone, with no cwd, branch or stamping involved, so an empty one can ONLY mean the
|
|
17
|
+
* lookup ROOT was wrong. Re-spawning cannot fix a wrong root, and the only escape was a symlink into
|
|
18
|
+
* `~/.claude` — i.e. hand-placing evidence inside a verification gate's own evidence tree.
|
|
19
|
+
*
|
|
20
|
+
* So the resolution lives here once, and the three modules call it. A second copy is how the first one
|
|
21
|
+
* got out of step.
|
|
22
|
+
*
|
|
23
|
+
* ─── WHY BOTH ROOTS ARE SEARCHED ─────────────────────────────────────────────────────────────────
|
|
24
|
+
* `$CLAUDE_CONFIG_DIR` is a property of the PROCESS, and the process that runs `wp-finish-upsert-pr`
|
|
25
|
+
* is not the process that wrote the transcripts. A shell launched before the variable was exported —
|
|
26
|
+
* or a hook, or a `pnpm` script whose env was sanitized — reads the transcripts of a session that had
|
|
27
|
+
* it set while itself having it unset, and vice versa. Resolving to exactly ONE root makes the check
|
|
28
|
+
* depend on env agreement between two unrelated processes, which is a fact nobody can verify and
|
|
29
|
+
* nobody can repair from inside the flow.
|
|
30
|
+
*
|
|
31
|
+
* Searching both is cheap (a `readdir` of a directory that usually does not exist) and it only ever
|
|
32
|
+
* ADDS candidate trees. It cannot weaken the integrity property either: every root yields the same
|
|
33
|
+
* harness-written `spawnDepth` / `isSidechain` / branch evidence, so a transcript found under the
|
|
34
|
+
* second root is no more forgeable than one found under the first.
|
|
35
|
+
*
|
|
36
|
+
* Shaped like {@link ClaudeEnv} in `claude-env.ts` — an injectable class plus one process-wide
|
|
37
|
+
* instance — because `no-function-outside-class` is on: module-scope helpers are not a shape this repo
|
|
38
|
+
* writes, whatever their arity.
|
|
39
|
+
*/
|
|
40
|
+
/** The env var Claude Code exports when its config tree has been relocated. */
|
|
41
|
+
export declare const CLAUDE_CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
|
|
42
|
+
export declare class ClaudeConfigDir {
|
|
43
|
+
/** The raw `$CLAUDE_CONFIG_DIR` value, or '' when it is not set. For DIAGNOSTICS, never for joining. */
|
|
44
|
+
configuredDir(): string;
|
|
45
|
+
/** Claude Code's config tree: `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`. */
|
|
46
|
+
root(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Every config tree worth searching: the configured one FIRST, then `~/.claude` when it is a
|
|
49
|
+
* different directory. See the both-roots note above for why this is a list and not one answer.
|
|
50
|
+
*/
|
|
51
|
+
roots(): string[];
|
|
52
|
+
/**
|
|
53
|
+
* `<config>/projects` for every root {@link roots} names, in the same order — where per-project
|
|
54
|
+
* session state and transcripts live.
|
|
55
|
+
*
|
|
56
|
+
* There is deliberately NO singular `projectsRoot()` beside this. Searching exactly one root is
|
|
57
|
+
* precisely the defect #963 was, so an exported spelling that returns the one-root answer would make
|
|
58
|
+
* the broken thing the shorter thing to type, in the one file that exists to prevent it.
|
|
59
|
+
*/
|
|
60
|
+
projectsRoots(): string[];
|
|
61
|
+
}
|
|
62
|
+
export declare const claudeConfigDir: ClaudeConfigDir;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claudeConfigDir = exports.ClaudeConfigDir = exports.CLAUDE_CONFIG_DIR_ENV = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const os = tslib_1.__importStar(require("os"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const inversify_1 = require("inversify");
|
|
8
|
+
/**
|
|
9
|
+
* THE ONE PLACE THAT KNOWS WHERE CLAUDE CODE KEEPS ITS ON-DISK STATE.
|
|
10
|
+
*
|
|
11
|
+
* ─── WHY THIS FILE EXISTS ────────────────────────────────────────────────────────────────────────
|
|
12
|
+
* `~/.claude` is a DEFAULT, not the location. Claude Code relocates the whole tree when
|
|
13
|
+
* `$CLAUDE_CONFIG_DIR` is set, and three modules in this package read that tree: reviewer provenance
|
|
14
|
+
* (`subagent-provenance.ts`), the audit record (`review-provenance.ts`) and the worktree-reap veto
|
|
15
|
+
* (`harness-agent-activity.ts`). Only the third one resolved it correctly; the other two joined
|
|
16
|
+
* `os.homedir()` with a hardcoded `'.claude'`.
|
|
17
|
+
*
|
|
18
|
+
* The consequence was not a degraded answer, it was a HARD BLOCK with no user-side workaround: on a
|
|
19
|
+
* machine with `CLAUDE_CONFIG_DIR=~/.claude-work`, provenance found zero transcripts — not the
|
|
20
|
+
* reviewers', not even the main agent's — so `wp-finish-upsert-pr` refused every PR whose reviewers
|
|
21
|
+
* had genuinely run and genuinely passed, and the refusal blamed the reviewers' cwd. The audit record
|
|
22
|
+
* it wrote said `mainTranscript: ""`, which is the field that settles it: the main transcript is found
|
|
23
|
+
* by session id alone, with no cwd, branch or stamping involved, so an empty one can ONLY mean the
|
|
24
|
+
* lookup ROOT was wrong. Re-spawning cannot fix a wrong root, and the only escape was a symlink into
|
|
25
|
+
* `~/.claude` — i.e. hand-placing evidence inside a verification gate's own evidence tree.
|
|
26
|
+
*
|
|
27
|
+
* So the resolution lives here once, and the three modules call it. A second copy is how the first one
|
|
28
|
+
* got out of step.
|
|
29
|
+
*
|
|
30
|
+
* ─── WHY BOTH ROOTS ARE SEARCHED ─────────────────────────────────────────────────────────────────
|
|
31
|
+
* `$CLAUDE_CONFIG_DIR` is a property of the PROCESS, and the process that runs `wp-finish-upsert-pr`
|
|
32
|
+
* is not the process that wrote the transcripts. A shell launched before the variable was exported —
|
|
33
|
+
* or a hook, or a `pnpm` script whose env was sanitized — reads the transcripts of a session that had
|
|
34
|
+
* it set while itself having it unset, and vice versa. Resolving to exactly ONE root makes the check
|
|
35
|
+
* depend on env agreement between two unrelated processes, which is a fact nobody can verify and
|
|
36
|
+
* nobody can repair from inside the flow.
|
|
37
|
+
*
|
|
38
|
+
* Searching both is cheap (a `readdir` of a directory that usually does not exist) and it only ever
|
|
39
|
+
* ADDS candidate trees. It cannot weaken the integrity property either: every root yields the same
|
|
40
|
+
* harness-written `spawnDepth` / `isSidechain` / branch evidence, so a transcript found under the
|
|
41
|
+
* second root is no more forgeable than one found under the first.
|
|
42
|
+
*
|
|
43
|
+
* Shaped like {@link ClaudeEnv} in `claude-env.ts` — an injectable class plus one process-wide
|
|
44
|
+
* instance — because `no-function-outside-class` is on: module-scope helpers are not a shape this repo
|
|
45
|
+
* writes, whatever their arity.
|
|
46
|
+
*/
|
|
47
|
+
/** The env var Claude Code exports when its config tree has been relocated. */
|
|
48
|
+
exports.CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';
|
|
49
|
+
let ClaudeConfigDir = class ClaudeConfigDir {
|
|
50
|
+
/** The raw `$CLAUDE_CONFIG_DIR` value, or '' when it is not set. For DIAGNOSTICS, never for joining. */
|
|
51
|
+
configuredDir() {
|
|
52
|
+
return (process.env[exports.CLAUDE_CONFIG_DIR_ENV] ?? '').trim();
|
|
53
|
+
}
|
|
54
|
+
/** Claude Code's config tree: `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`. */
|
|
55
|
+
root() {
|
|
56
|
+
const configured = this.configuredDir();
|
|
57
|
+
return configured !== '' ? configured : path.join(os.homedir(), '.claude');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Every config tree worth searching: the configured one FIRST, then `~/.claude` when it is a
|
|
61
|
+
* different directory. See the both-roots note above for why this is a list and not one answer.
|
|
62
|
+
*/
|
|
63
|
+
roots() {
|
|
64
|
+
const configured = this.root();
|
|
65
|
+
const fallback = path.join(os.homedir(), '.claude');
|
|
66
|
+
return path.resolve(configured) === path.resolve(fallback) ? [configured] : [configured, fallback];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `<config>/projects` for every root {@link roots} names, in the same order — where per-project
|
|
70
|
+
* session state and transcripts live.
|
|
71
|
+
*
|
|
72
|
+
* There is deliberately NO singular `projectsRoot()` beside this. Searching exactly one root is
|
|
73
|
+
* precisely the defect #963 was, so an exported spelling that returns the one-root answer would make
|
|
74
|
+
* the broken thing the shorter thing to type, in the one file that exists to prevent it.
|
|
75
|
+
*/
|
|
76
|
+
projectsRoots() {
|
|
77
|
+
return this.roots().map((root) => path.join(root, 'projects'));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
exports.ClaudeConfigDir = ClaudeConfigDir;
|
|
81
|
+
exports.ClaudeConfigDir = ClaudeConfigDir = tslib_1.__decorate([
|
|
82
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
83
|
+
], ClaudeConfigDir);
|
|
84
|
+
// Process-wide instance for the many non-DI call sites (hooks, wp-* bins); inversify still injects the
|
|
85
|
+
// singleton wherever a container is in play. Same arrangement as `claudeEnv`.
|
|
86
|
+
exports.claudeConfigDir = new ClaudeConfigDir();
|
|
87
|
+
//# sourceMappingURL=claude-config-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-config-dir.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/claude-config-dir.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,yCAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,+EAA+E;AAClE,QAAA,qBAAqB,GAAG,mBAAmB,CAAC;AAGlD,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB,wGAAwG;IACxG,aAAa;QACT,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,6BAAqB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7D,CAAC;IAED,kFAAkF;IAClF,IAAI;QACA,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACxC,OAAO,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IAC/E,CAAC;IAED;;;OAGG;IACH,KAAK;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvG,CAAC;IAED;;;;;;;OAOG;IACH,aAAa;QACT,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;IACnF,CAAC;CACJ,CAAA;AAjCY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAiC3B;AAED,uGAAuG;AACvG,8EAA8E;AACjE,QAAA,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC","sourcesContent":["import * as os from 'os';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * THE ONE PLACE THAT KNOWS WHERE CLAUDE CODE KEEPS ITS ON-DISK STATE.\n *\n * ─── WHY THIS FILE EXISTS ────────────────────────────────────────────────────────────────────────\n * `~/.claude` is a DEFAULT, not the location. Claude Code relocates the whole tree when\n * `$CLAUDE_CONFIG_DIR` is set, and three modules in this package read that tree: reviewer provenance\n * (`subagent-provenance.ts`), the audit record (`review-provenance.ts`) and the worktree-reap veto\n * (`harness-agent-activity.ts`). Only the third one resolved it correctly; the other two joined\n * `os.homedir()` with a hardcoded `'.claude'`.\n *\n * The consequence was not a degraded answer, it was a HARD BLOCK with no user-side workaround: on a\n * machine with `CLAUDE_CONFIG_DIR=~/.claude-work`, provenance found zero transcripts — not the\n * reviewers', not even the main agent's — so `wp-finish-upsert-pr` refused every PR whose reviewers\n * had genuinely run and genuinely passed, and the refusal blamed the reviewers' cwd. The audit record\n * it wrote said `mainTranscript: \"\"`, which is the field that settles it: the main transcript is found\n * by session id alone, with no cwd, branch or stamping involved, so an empty one can ONLY mean the\n * lookup ROOT was wrong. Re-spawning cannot fix a wrong root, and the only escape was a symlink into\n * `~/.claude` — i.e. hand-placing evidence inside a verification gate's own evidence tree.\n *\n * So the resolution lives here once, and the three modules call it. A second copy is how the first one\n * got out of step.\n *\n * ─── WHY BOTH ROOTS ARE SEARCHED ─────────────────────────────────────────────────────────────────\n * `$CLAUDE_CONFIG_DIR` is a property of the PROCESS, and the process that runs `wp-finish-upsert-pr`\n * is not the process that wrote the transcripts. A shell launched before the variable was exported —\n * or a hook, or a `pnpm` script whose env was sanitized — reads the transcripts of a session that had\n * it set while itself having it unset, and vice versa. Resolving to exactly ONE root makes the check\n * depend on env agreement between two unrelated processes, which is a fact nobody can verify and\n * nobody can repair from inside the flow.\n *\n * Searching both is cheap (a `readdir` of a directory that usually does not exist) and it only ever\n * ADDS candidate trees. It cannot weaken the integrity property either: every root yields the same\n * harness-written `spawnDepth` / `isSidechain` / branch evidence, so a transcript found under the\n * second root is no more forgeable than one found under the first.\n *\n * Shaped like {@link ClaudeEnv} in `claude-env.ts` — an injectable class plus one process-wide\n * instance — because `no-function-outside-class` is on: module-scope helpers are not a shape this repo\n * writes, whatever their arity.\n */\n\n/** The env var Claude Code exports when its config tree has been relocated. */\nexport const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';\n\n@injectable(bindingScopeValues.Singleton)\nexport class ClaudeConfigDir {\n /** The raw `$CLAUDE_CONFIG_DIR` value, or '' when it is not set. For DIAGNOSTICS, never for joining. */\n configuredDir(): string {\n return (process.env[CLAUDE_CONFIG_DIR_ENV] ?? '').trim();\n }\n\n /** Claude Code's config tree: `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`. */\n root(): string {\n const configured = this.configuredDir();\n return configured !== '' ? configured : path.join(os.homedir(), '.claude');\n }\n\n /**\n * Every config tree worth searching: the configured one FIRST, then `~/.claude` when it is a\n * different directory. See the both-roots note above for why this is a list and not one answer.\n */\n roots(): string[] {\n const configured = this.root();\n const fallback = path.join(os.homedir(), '.claude');\n return path.resolve(configured) === path.resolve(fallback) ? [configured] : [configured, fallback];\n }\n\n /**\n * `<config>/projects` for every root {@link roots} names, in the same order — where per-project\n * session state and transcripts live.\n *\n * There is deliberately NO singular `projectsRoot()` beside this. Searching exactly one root is\n * precisely the defect #963 was, so an exported spelling that returns the one-root answer would make\n * the broken thing the shorter thing to type, in the one file that exists to prevent it.\n */\n projectsRoots(): string[] {\n return this.roots().map((root: string): string => path.join(root, 'projects'));\n }\n}\n\n// Process-wide instance for the many non-DI call sites (hooks, wp-* bins); inversify still injects the\n// singleton wherever a container is in play. Same arrangement as `claudeEnv`.\nexport const claudeConfigDir = new ClaudeConfigDir();\n"]}
|
|
@@ -130,10 +130,12 @@ export declare class HarnessAgentActivityReader {
|
|
|
130
130
|
/** Every `<config>/projects/<slug>/<session>/subagents` directory that exists right now. */
|
|
131
131
|
private subagentDirs;
|
|
132
132
|
/**
|
|
133
|
-
* Seam: where the harness keeps its per-project state
|
|
134
|
-
*
|
|
133
|
+
* Seam: where the harness keeps its per-project state, delegated to the ONE resolver
|
|
134
|
+
* ({@link ClaudeConfigDir}) so this file and the two provenance modules cannot disagree about
|
|
135
|
+
* it — they did, and the divergence hard-blocked every PR on a relocated config dir. Specs point
|
|
136
|
+
* `$CLAUDE_CONFIG_DIR` at a fixture tree, which exercises the same resolution the field uses.
|
|
135
137
|
*/
|
|
136
|
-
protected
|
|
138
|
+
protected projectsRoots(): string[];
|
|
137
139
|
private minutesAgo;
|
|
138
140
|
private exists;
|
|
139
141
|
private readMeta;
|
|
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.HarnessAgentActivityReader = exports.AgentActivity = exports.AGENT_TRANSCRIPT_QUIET_MS = exports.AGENT_ACTIVITY_UNKNOWN = exports.AGENT_ACTIVITY_RETURNED = exports.AGENT_ACTIVITY_LIVE = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
-
const os = tslib_1.__importStar(require("os"));
|
|
7
6
|
const path = tslib_1.__importStar(require("path"));
|
|
8
7
|
const inversify_1 = require("inversify");
|
|
8
|
+
const claude_config_dir_1 = require("./claude-config-dir");
|
|
9
9
|
const to_error_1 = require("./to-error");
|
|
10
10
|
/**
|
|
11
11
|
* THE ONE PLACE THAT KNOWS ANYTHING ABOUT CLAUDE CODE'S ON-DISK STATE.
|
|
@@ -216,25 +216,26 @@ let HarnessAgentActivityReader = class HarnessAgentActivityReader {
|
|
|
216
216
|
/** Every `<config>/projects/<slug>/<session>/subagents` directory that exists right now. */
|
|
217
217
|
subagentDirs() {
|
|
218
218
|
const out = [];
|
|
219
|
-
const root
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
219
|
+
for (const root of this.projectsRoots()) {
|
|
220
|
+
for (const project of this.readDir(root)) {
|
|
221
|
+
const projectDir = path.join(root, project);
|
|
222
|
+
for (const session of this.readDir(projectDir)) {
|
|
223
|
+
const subagents = path.join(projectDir, session, 'subagents');
|
|
224
|
+
if (this.isDirectory(subagents))
|
|
225
|
+
out.push(subagents);
|
|
226
|
+
}
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
return out;
|
|
229
230
|
}
|
|
230
231
|
/**
|
|
231
|
-
* Seam: where the harness keeps its per-project state
|
|
232
|
-
*
|
|
232
|
+
* Seam: where the harness keeps its per-project state, delegated to the ONE resolver
|
|
233
|
+
* ({@link ClaudeConfigDir}) so this file and the two provenance modules cannot disagree about
|
|
234
|
+
* it — they did, and the divergence hard-blocked every PR on a relocated config dir. Specs point
|
|
235
|
+
* `$CLAUDE_CONFIG_DIR` at a fixture tree, which exercises the same resolution the field uses.
|
|
233
236
|
*/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const root = configured !== '' ? configured : path.join(os.homedir(), '.claude');
|
|
237
|
-
return path.join(root, 'projects');
|
|
237
|
+
projectsRoots() {
|
|
238
|
+
return claude_config_dir_1.claudeConfigDir.projectsRoots();
|
|
238
239
|
}
|
|
239
240
|
minutesAgo(now, written) {
|
|
240
241
|
const minutes = Math.max(0, Math.floor((now - written) / 60000));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-agent-activity.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/harness-agent-activity.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { toError } from './to-error';\n\n/**\n * THE ONE PLACE THAT KNOWS ANYTHING ABOUT CLAUDE CODE'S ON-DISK STATE.\n *\n * ─── THE RULE THIS CLASS EXISTS UNDER ────────────────────────────────────────────────────────────\n *\n * HARNESS STATE MAY ONLY VETO A REAP. IT MAY NEVER LICENSE ONE.\n *\n * Read that before reading anything else here, because every answer below is shaped by it. The\n * licence to remove a worktree comes SOLELY from evidence wp-cleanup already computes and already\n * gets right: a merged PR, a snapshot of a ref that still holds the work, a ref identical to\n * origin/main, plus a clean `git status --porcelain`. This class answers ONE question — \"is somebody\n * plausibly still in there?\" — and its only power is to STOP a reap that the branch evidence had\n * already authorised. No answer it can give turns a spared worktree into a reaped one.\n *\n * ─── WHY IT EXISTS ───────────────────────────────────────────────────────────────────────────────\n * `git worktree lock` reasons written by the Claude Code harness carry a pid, and wp-cleanup used to\n * ask the kernel whether that pid was alive. It always is. Subagents are NOT separate OS processes —\n * every agent in a session records the SAME pid, the session process — so \"is that pid running?\"\n * reduced to \"is the editor still open?\", which is true by construction for the whole life of a\n * session. Observed in monorepo-nx2: nine worktrees against a cap of five, eight of them reported as\n * \"that agent is working in here\" with one identical pid, exactly ONE agent actually running, and\n * several of the others' PRs already merged.\n *\n * So a veto has to come from somewhere that knows about AGENTS rather than processes. The harness\n * does, and it writes it down:\n *\n * <config>/projects/<project-slug>/<sessionId>/subagents/agent-<id>.meta.json\n * <config>/projects/<project-slug>/<sessionId>/subagents/agent-<id>.jsonl\n *\n * `<config>` is `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`. `agent-<id>` is ONE token, and it is\n * the same token in three places: the stem of those two filenames, the basename of the `worktreePath`\n * that meta records, and the name in the lock reason `claude agent agent-<id> (pid N …)`. So worktree\n * → agent state is an exact lookup, not a heuristic — and nothing here re-derives or re-prefixes that\n * token, because a filename built from half of it is how a lookup quietly becomes a miss.\n *\n * ─── THE SIGNAL: THE SHAPE OF THE LAST RECORD IN THE AGENT'S OWN TRANSCRIPT ──────────────────────\n * An agent's loop pauses when the model emits text and asks for no tool. That is the definition of\n * the loop, not a correlation with one:\n *\n * last record is `assistant` with NO `tool_use` block → RETURNED — not currently in a tool loop\n * anything else (a `tool_use` block, a `user`/tool_result record) → MID-LOOP\n *\n * Verified on three agents known to have finished (all `assistant` / `['text']`) and on a live one\n * (`assistant` / `['tool_use']`). `thinking` blocks appear alongside `text` and change nothing: the\n * criterion is the ABSENCE of `tool_use`, never \"exactly one block\". A `user` record's content can be\n * a plain STRING rather than a list of blocks; that is mid-loop too, and must not throw.\n *\n * ─── \"RETURNED\" IS NOT \"DONE FOREVER\", WHICH IS WHY IT CANNOT LICENSE A REAP ──────────────────────\n * Measured: a throwaway agent returned with a text-only record at 17:04:20, and at 17:08:11 a `user`\n * record appeared and it ran again, returning a second time at 17:08:13. The harness RE-INVOKES an\n * agent when a background child it started completes, and when a parent sends it a message. It holds\n * its worktree across that gap. So RETURNED means \"not in a tool loop right now\" and nothing\n * stronger — which is survivable only because of the rule at the top of this file: the reap was\n * already authorised by a merged PR and a clean tree, and an agent that briefly resumes into that\n * situation has nothing to lose.\n *\n * ─── WHY MTIME IS ONLY A TIEBREAKER ──────────────────────────────────────────────────────────────\n * Freshness of the transcript looks like the obvious liveness signal and is NOT one. Measured: one\n * minute after that throwaway agent finished, its mtime was FRESHER than the genuinely live agent's\n * had been moments earlier. And a live agent writes nothing at all while it sits inside one long Bash\n * call — a `wp-build` is ten minutes of silence — so any threshold safe against that also covers\n * every just-finished agent, which is precisely the population wp-cleanup meets. Mtime alone does not\n * fix this bug; it re-times it. It earns its place on exactly one case: telling a mid-loop transcript\n * that is being written RIGHT NOW (a live agent, veto) from one frozen mid-`tool_use` by a killed\n * session (no veto — that is the original \"looks live forever\" defect).\n *\n * ─── NEGATIVE RESULT — DO NOT RE-TRY THIS ────────────────────────────────────────────────────────\n * The meta records a `toolUseId`, and a `tool_result` for it in the SPAWNER's transcript looks like an\n * exact completion marker. It is NOT one: a backgrounded Agent call gets its \"launched successfully\"\n * tool_result IMMEDIATELY, so a running agent already has one, indistinguishable from a finished\n * agent's. Measured on a live agent and rejected.\n *\n * ─── EVERY ANSWER FAILS SAFE ─────────────────────────────────────────────────────────────────────\n * A missing config dir, an unreadable file, a meta describing a different worktree, a truncated last\n * line — all of them are UNKNOWN. UNKNOWN withholds the veto, which is only ever safe because the\n * branch evidence had to authorise the reap first.\n *\n * THIS IS UNDOCUMENTED HARNESS INTERNALS, deliberately quarantined in one file so a layout change\n * breaks exactly here and degrades to \"cannot tell\" everywhere else.\n */\n\n/** Mid-loop and being written to right now. The ONE answer that vetoes a reap. */\nexport const AGENT_ACTIVITY_LIVE = 'live';\n/** Its transcript ends with the model returning — not in a tool loop. It may still resume. */\nexport const AGENT_ACTIVITY_RETURNED = 'returned';\n/** No usable evidence, or a mid-loop transcript nobody has touched for a long time. */\nexport const AGENT_ACTIVITY_UNKNOWN = 'unknown';\n\n/**\n * How long a MID-LOOP transcript may sit untouched before its silence stops vetoing a reap.\n *\n * Consulted for a mid-loop transcript only — a returned one is recognised by shape, whatever its\n * mtime — so this number decides exactly one thing: how long an agent that was KILLED inside a tool\n * call goes on vetoing. Generous on purpose, because a live agent writes NOTHING while it is inside\n * one long Bash call and a `wp-build` or `nx run … :ci` is routinely ten minutes of that. A threshold\n * in seconds would drop the veto on a building agent, which is the one direction that costs work;\n * being late costs a directory the next cleanup takes.\n */\nexport const AGENT_TRANSCRIPT_QUIET_MS = 45 * 60 * 1000;\n\n/**\n * How much of the tail of a transcript is read to find its last record.\n *\n * Transcripts are append-only and reach tens of megabytes; the answer lives in the final line. Large\n * enough that a single fat record (a big tool result) still fits whole — and when it does not, the\n * fragment fails to parse and the answer is UNKNOWN.\n */\nconst TRANSCRIPT_TAIL_BYTES = 8 * 1024 * 1024;\n\n// Data-only (per CLAUDE.md, classes for data). What the harness says about one agent, and why.\nexport class AgentActivity {\n /** One of the AGENT_ACTIVITY_* constants. */\n state: string;\n /** Human-readable evidence, printed verbatim into wp-cleanup's spared/overridden reasons. */\n detail: string;\n\n constructor(state: string, detail: string) {\n this.state = state;\n this.detail = detail;\n }\n}\n\n/**\n * Data-only. Where an agent's transcript is — or, when it is nowhere, WHICH of the three genuinely\n * different silences we hit.\n *\n * One flat \"no state file for that agent id\" covered all three, and two of them were untrue: a meta\n * that exists and cannot be PARSED, and a meta that exists and describes SOMEBODY ELSE'S worktree.\n * That string is printed verbatim into wp-cleanup's reason, so it is a message asserting more than\n * the evidence supports — the exact defect this whole file exists to remove, small and on the safe\n * side but the same shape.\n */\nclass AgentStateLookup {\n /** The agent's own transcript, or '' when there is none to read. */\n transcript: string;\n /** When `transcript` is '': the honest reason, printed to a human as-is. */\n detail: string;\n\n constructor(transcript: string, detail: string) {\n this.transcript = transcript;\n this.detail = detail;\n }\n}\n\n// Raw JSON shapes for the cast at the parse boundary — the convention merged-branches.ts already uses\n// for files it revives. Every field optional: this is somebody else's format.\ninterface RawAgentMeta {\n worktreePath?: string;\n}\n\ninterface RawContentBlock {\n type?: string;\n}\n\ninterface RawTranscriptMessage {\n // A `user` record's content is sometimes a plain string rather than a list of blocks.\n content?: RawContentBlock[] | string;\n}\n\ninterface RawTranscriptRecord {\n type?: string;\n message?: RawTranscriptMessage;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class HarnessAgentActivityReader {\n /**\n * What the harness says about `agentId`, cross-checked against the worktree we are judging.\n *\n * `worktreePath` is not decoration: it is what makes this a LOOKUP rather than a guess. If the\n * meta we find records a different worktree we have matched the wrong thing, and the answer is\n * UNKNOWN — never an answer about somebody else's directory.\n */\n activityOf(agentId: string, worktreePath: string, now: number = Date.now()): AgentActivity {\n const found = this.locate(agentId, worktreePath);\n if (found.transcript === '') return new AgentActivity(AGENT_ACTIVITY_UNKNOWN, found.detail);\n const last = this.lastRecord(found.transcript);\n if (last === null) {\n return new AgentActivity(AGENT_ACTIVITY_UNKNOWN, 'its transcript could not be read to the end');\n }\n if (this.isReturn(last)) {\n return new AgentActivity(AGENT_ACTIVITY_RETURNED,\n 'its transcript ends with that agent returning its answer, so it is not in a tool call');\n }\n const written = this.mtimeOf(found.transcript);\n if (written > 0 && now - written < AGENT_TRANSCRIPT_QUIET_MS) {\n return new AgentActivity(AGENT_ACTIVITY_LIVE,\n `its transcript ends mid-tool-call and was written ${this.minutesAgo(now, written)}`);\n }\n return new AgentActivity(AGENT_ACTIVITY_UNKNOWN,\n 'its transcript ends mid-tool-call but nothing has written to it for a long time, so that '\n + 'agent was more likely killed than working');\n }\n\n /**\n * Did the agent RETURN? True only for an `assistant` record whose content asks for no tool.\n *\n * A `user` record (a tool result coming back, or a message sent to the agent) is mid-loop by\n * definition — and its content may be a plain string, which is why the array check comes first. So\n * is an `assistant` record carrying a `tool_use` EVEN ALONGSIDE TEXT: the model narrating before\n * it calls something is not a return.\n */\n private isReturn(record: RawTranscriptRecord): boolean {\n if ((record.type ?? '') !== 'assistant') return false;\n const content = record.message === undefined ? undefined : record.message.content;\n if (!Array.isArray(content) || content.length === 0) return false;\n for (const block of content) {\n if ((block.type ?? '') === 'tool_use') return false;\n }\n return true;\n }\n\n /**\n * The agent's own transcript, or the reason there is none — each of the three exits phrased for\n * what actually happened. `exists` is asked BEFORE `readMeta` precisely so \"not there\" and\n * \"there and unreadable\" stay distinguishable; folding them together is what produced the flat\n * over-claim this replaced.\n */\n private locate(agentId: string, worktreePath: string): AgentStateLookup {\n for (const subagents of this.subagentDirs()) {\n const metaPath = path.join(subagents, `${agentId}.meta.json`);\n if (!this.exists(metaPath)) continue;\n const meta = this.readMeta(metaPath);\n if (meta === null) {\n return new AgentStateLookup('',\n `that agent's harness state file (${metaPath}) could not be read`);\n }\n const recorded = meta.worktreePath ?? '';\n if (recorded !== '' && worktreePath !== '' && path.resolve(recorded) !== path.resolve(worktreePath)) {\n return new AgentStateLookup('',\n `the harness records that agent against a different worktree (${recorded})`);\n }\n return new AgentStateLookup(path.join(subagents, `${agentId}.jsonl`), '');\n }\n return new AgentStateLookup('', 'the Claude Code harness has no state file for that agent id');\n }\n\n /** Every `<config>/projects/<slug>/<session>/subagents` directory that exists right now. */\n private subagentDirs(): string[] {\n const out: string[] = [];\n const root = this.projectsRoot();\n for (const project of this.readDir(root)) {\n const projectDir = path.join(root, project);\n for (const session of this.readDir(projectDir)) {\n const subagents = path.join(projectDir, session, 'subagents');\n if (this.isDirectory(subagents)) out.push(subagents);\n }\n }\n return out;\n }\n\n /**\n * Seam: where the harness keeps its per-project state. `$CLAUDE_CONFIG_DIR` wins when set, which\n * is how the harness itself resolves it; specs override this to point at a fixture tree.\n */\n protected projectsRoot(): string {\n const configured = process.env['CLAUDE_CONFIG_DIR'] ?? '';\n const root = configured !== '' ? configured : path.join(os.homedir(), '.claude');\n return path.join(root, 'projects');\n }\n\n private minutesAgo(now: number, written: number): string {\n const minutes = Math.max(0, Math.floor((now - written) / 60000));\n return minutes === 1 ? '1 minute ago' : `${String(minutes)} minutes ago`;\n }\n\n // ── Everything below is a guarded filesystem read: any failure is silence, never a throw. ──\n\n private exists(filePath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(filePath);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // null means \"it is there and this release could not make sense of it\" — NOT \"it is absent\".\n private readMeta(metaPath: string): RawAgentMeta | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(metaPath, 'utf8')) as RawAgentMeta;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * The last complete JSONL record of `transcript`, or null.\n *\n * Reads only the tail, because these files grow without bound and only the final line matters. A\n * window that starts mid-record yields an unparseable fragment and therefore null — \"cannot tell\"\n * — which is exactly what a truncated or half-written last line should also produce.\n */\n private lastRecord(transcript: string): RawTranscriptRecord | null {\n const lines = this.readTail(transcript).split('\\n')\n .filter((line: string): boolean => line.trim() !== '');\n if (lines.length === 0) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(lines[lines.length - 1]) as RawTranscriptRecord;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private readTail(filePath: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const size = fs.statSync(filePath).size;\n const length = Math.min(size, TRANSCRIPT_TAIL_BYTES);\n const buffer = Buffer.alloc(length);\n const fd = fs.openSync(filePath, 'r');\n fs.readSync(fd, buffer, 0, length, size - length);\n fs.closeSync(fd);\n return buffer.toString('utf8');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n private readDir(dirPath: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dirPath);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private isDirectory(dirPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(dirPath).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // 0 when the file is absent or unreadable — which reads as \"no evidence\", not \"written long ago\".\n private mtimeOf(filePath: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(filePath).mtimeMs;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 0;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"harness-agent-activity.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/harness-agent-activity.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 { claudeConfigDir } from './claude-config-dir';\nimport { toError } from './to-error';\n\n/**\n * THE ONE PLACE THAT KNOWS ANYTHING ABOUT CLAUDE CODE'S ON-DISK STATE.\n *\n * ─── THE RULE THIS CLASS EXISTS UNDER ────────────────────────────────────────────────────────────\n *\n * HARNESS STATE MAY ONLY VETO A REAP. IT MAY NEVER LICENSE ONE.\n *\n * Read that before reading anything else here, because every answer below is shaped by it. The\n * licence to remove a worktree comes SOLELY from evidence wp-cleanup already computes and already\n * gets right: a merged PR, a snapshot of a ref that still holds the work, a ref identical to\n * origin/main, plus a clean `git status --porcelain`. This class answers ONE question — \"is somebody\n * plausibly still in there?\" — and its only power is to STOP a reap that the branch evidence had\n * already authorised. No answer it can give turns a spared worktree into a reaped one.\n *\n * ─── WHY IT EXISTS ───────────────────────────────────────────────────────────────────────────────\n * `git worktree lock` reasons written by the Claude Code harness carry a pid, and wp-cleanup used to\n * ask the kernel whether that pid was alive. It always is. Subagents are NOT separate OS processes —\n * every agent in a session records the SAME pid, the session process — so \"is that pid running?\"\n * reduced to \"is the editor still open?\", which is true by construction for the whole life of a\n * session. Observed in monorepo-nx2: nine worktrees against a cap of five, eight of them reported as\n * \"that agent is working in here\" with one identical pid, exactly ONE agent actually running, and\n * several of the others' PRs already merged.\n *\n * So a veto has to come from somewhere that knows about AGENTS rather than processes. The harness\n * does, and it writes it down:\n *\n * <config>/projects/<project-slug>/<sessionId>/subagents/agent-<id>.meta.json\n * <config>/projects/<project-slug>/<sessionId>/subagents/agent-<id>.jsonl\n *\n * `<config>` is `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`. `agent-<id>` is ONE token, and it is\n * the same token in three places: the stem of those two filenames, the basename of the `worktreePath`\n * that meta records, and the name in the lock reason `claude agent agent-<id> (pid N …)`. So worktree\n * → agent state is an exact lookup, not a heuristic — and nothing here re-derives or re-prefixes that\n * token, because a filename built from half of it is how a lookup quietly becomes a miss.\n *\n * ─── THE SIGNAL: THE SHAPE OF THE LAST RECORD IN THE AGENT'S OWN TRANSCRIPT ──────────────────────\n * An agent's loop pauses when the model emits text and asks for no tool. That is the definition of\n * the loop, not a correlation with one:\n *\n * last record is `assistant` with NO `tool_use` block → RETURNED — not currently in a tool loop\n * anything else (a `tool_use` block, a `user`/tool_result record) → MID-LOOP\n *\n * Verified on three agents known to have finished (all `assistant` / `['text']`) and on a live one\n * (`assistant` / `['tool_use']`). `thinking` blocks appear alongside `text` and change nothing: the\n * criterion is the ABSENCE of `tool_use`, never \"exactly one block\". A `user` record's content can be\n * a plain STRING rather than a list of blocks; that is mid-loop too, and must not throw.\n *\n * ─── \"RETURNED\" IS NOT \"DONE FOREVER\", WHICH IS WHY IT CANNOT LICENSE A REAP ──────────────────────\n * Measured: a throwaway agent returned with a text-only record at 17:04:20, and at 17:08:11 a `user`\n * record appeared and it ran again, returning a second time at 17:08:13. The harness RE-INVOKES an\n * agent when a background child it started completes, and when a parent sends it a message. It holds\n * its worktree across that gap. So RETURNED means \"not in a tool loop right now\" and nothing\n * stronger — which is survivable only because of the rule at the top of this file: the reap was\n * already authorised by a merged PR and a clean tree, and an agent that briefly resumes into that\n * situation has nothing to lose.\n *\n * ─── WHY MTIME IS ONLY A TIEBREAKER ──────────────────────────────────────────────────────────────\n * Freshness of the transcript looks like the obvious liveness signal and is NOT one. Measured: one\n * minute after that throwaway agent finished, its mtime was FRESHER than the genuinely live agent's\n * had been moments earlier. And a live agent writes nothing at all while it sits inside one long Bash\n * call — a `wp-build` is ten minutes of silence — so any threshold safe against that also covers\n * every just-finished agent, which is precisely the population wp-cleanup meets. Mtime alone does not\n * fix this bug; it re-times it. It earns its place on exactly one case: telling a mid-loop transcript\n * that is being written RIGHT NOW (a live agent, veto) from one frozen mid-`tool_use` by a killed\n * session (no veto — that is the original \"looks live forever\" defect).\n *\n * ─── NEGATIVE RESULT — DO NOT RE-TRY THIS ────────────────────────────────────────────────────────\n * The meta records a `toolUseId`, and a `tool_result` for it in the SPAWNER's transcript looks like an\n * exact completion marker. It is NOT one: a backgrounded Agent call gets its \"launched successfully\"\n * tool_result IMMEDIATELY, so a running agent already has one, indistinguishable from a finished\n * agent's. Measured on a live agent and rejected.\n *\n * ─── EVERY ANSWER FAILS SAFE ─────────────────────────────────────────────────────────────────────\n * A missing config dir, an unreadable file, a meta describing a different worktree, a truncated last\n * line — all of them are UNKNOWN. UNKNOWN withholds the veto, which is only ever safe because the\n * branch evidence had to authorise the reap first.\n *\n * THIS IS UNDOCUMENTED HARNESS INTERNALS, deliberately quarantined in one file so a layout change\n * breaks exactly here and degrades to \"cannot tell\" everywhere else.\n */\n\n/** Mid-loop and being written to right now. The ONE answer that vetoes a reap. */\nexport const AGENT_ACTIVITY_LIVE = 'live';\n/** Its transcript ends with the model returning — not in a tool loop. It may still resume. */\nexport const AGENT_ACTIVITY_RETURNED = 'returned';\n/** No usable evidence, or a mid-loop transcript nobody has touched for a long time. */\nexport const AGENT_ACTIVITY_UNKNOWN = 'unknown';\n\n/**\n * How long a MID-LOOP transcript may sit untouched before its silence stops vetoing a reap.\n *\n * Consulted for a mid-loop transcript only — a returned one is recognised by shape, whatever its\n * mtime — so this number decides exactly one thing: how long an agent that was KILLED inside a tool\n * call goes on vetoing. Generous on purpose, because a live agent writes NOTHING while it is inside\n * one long Bash call and a `wp-build` or `nx run … :ci` is routinely ten minutes of that. A threshold\n * in seconds would drop the veto on a building agent, which is the one direction that costs work;\n * being late costs a directory the next cleanup takes.\n */\nexport const AGENT_TRANSCRIPT_QUIET_MS = 45 * 60 * 1000;\n\n/**\n * How much of the tail of a transcript is read to find its last record.\n *\n * Transcripts are append-only and reach tens of megabytes; the answer lives in the final line. Large\n * enough that a single fat record (a big tool result) still fits whole — and when it does not, the\n * fragment fails to parse and the answer is UNKNOWN.\n */\nconst TRANSCRIPT_TAIL_BYTES = 8 * 1024 * 1024;\n\n// Data-only (per CLAUDE.md, classes for data). What the harness says about one agent, and why.\nexport class AgentActivity {\n /** One of the AGENT_ACTIVITY_* constants. */\n state: string;\n /** Human-readable evidence, printed verbatim into wp-cleanup's spared/overridden reasons. */\n detail: string;\n\n constructor(state: string, detail: string) {\n this.state = state;\n this.detail = detail;\n }\n}\n\n/**\n * Data-only. Where an agent's transcript is — or, when it is nowhere, WHICH of the three genuinely\n * different silences we hit.\n *\n * One flat \"no state file for that agent id\" covered all three, and two of them were untrue: a meta\n * that exists and cannot be PARSED, and a meta that exists and describes SOMEBODY ELSE'S worktree.\n * That string is printed verbatim into wp-cleanup's reason, so it is a message asserting more than\n * the evidence supports — the exact defect this whole file exists to remove, small and on the safe\n * side but the same shape.\n */\nclass AgentStateLookup {\n /** The agent's own transcript, or '' when there is none to read. */\n transcript: string;\n /** When `transcript` is '': the honest reason, printed to a human as-is. */\n detail: string;\n\n constructor(transcript: string, detail: string) {\n this.transcript = transcript;\n this.detail = detail;\n }\n}\n\n// Raw JSON shapes for the cast at the parse boundary — the convention merged-branches.ts already uses\n// for files it revives. Every field optional: this is somebody else's format.\ninterface RawAgentMeta {\n worktreePath?: string;\n}\n\ninterface RawContentBlock {\n type?: string;\n}\n\ninterface RawTranscriptMessage {\n // A `user` record's content is sometimes a plain string rather than a list of blocks.\n content?: RawContentBlock[] | string;\n}\n\ninterface RawTranscriptRecord {\n type?: string;\n message?: RawTranscriptMessage;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class HarnessAgentActivityReader {\n /**\n * What the harness says about `agentId`, cross-checked against the worktree we are judging.\n *\n * `worktreePath` is not decoration: it is what makes this a LOOKUP rather than a guess. If the\n * meta we find records a different worktree we have matched the wrong thing, and the answer is\n * UNKNOWN — never an answer about somebody else's directory.\n */\n activityOf(agentId: string, worktreePath: string, now: number = Date.now()): AgentActivity {\n const found = this.locate(agentId, worktreePath);\n if (found.transcript === '') return new AgentActivity(AGENT_ACTIVITY_UNKNOWN, found.detail);\n const last = this.lastRecord(found.transcript);\n if (last === null) {\n return new AgentActivity(AGENT_ACTIVITY_UNKNOWN, 'its transcript could not be read to the end');\n }\n if (this.isReturn(last)) {\n return new AgentActivity(AGENT_ACTIVITY_RETURNED,\n 'its transcript ends with that agent returning its answer, so it is not in a tool call');\n }\n const written = this.mtimeOf(found.transcript);\n if (written > 0 && now - written < AGENT_TRANSCRIPT_QUIET_MS) {\n return new AgentActivity(AGENT_ACTIVITY_LIVE,\n `its transcript ends mid-tool-call and was written ${this.minutesAgo(now, written)}`);\n }\n return new AgentActivity(AGENT_ACTIVITY_UNKNOWN,\n 'its transcript ends mid-tool-call but nothing has written to it for a long time, so that '\n + 'agent was more likely killed than working');\n }\n\n /**\n * Did the agent RETURN? True only for an `assistant` record whose content asks for no tool.\n *\n * A `user` record (a tool result coming back, or a message sent to the agent) is mid-loop by\n * definition — and its content may be a plain string, which is why the array check comes first. So\n * is an `assistant` record carrying a `tool_use` EVEN ALONGSIDE TEXT: the model narrating before\n * it calls something is not a return.\n */\n private isReturn(record: RawTranscriptRecord): boolean {\n if ((record.type ?? '') !== 'assistant') return false;\n const content = record.message === undefined ? undefined : record.message.content;\n if (!Array.isArray(content) || content.length === 0) return false;\n for (const block of content) {\n if ((block.type ?? '') === 'tool_use') return false;\n }\n return true;\n }\n\n /**\n * The agent's own transcript, or the reason there is none — each of the three exits phrased for\n * what actually happened. `exists` is asked BEFORE `readMeta` precisely so \"not there\" and\n * \"there and unreadable\" stay distinguishable; folding them together is what produced the flat\n * over-claim this replaced.\n */\n private locate(agentId: string, worktreePath: string): AgentStateLookup {\n for (const subagents of this.subagentDirs()) {\n const metaPath = path.join(subagents, `${agentId}.meta.json`);\n if (!this.exists(metaPath)) continue;\n const meta = this.readMeta(metaPath);\n if (meta === null) {\n return new AgentStateLookup('',\n `that agent's harness state file (${metaPath}) could not be read`);\n }\n const recorded = meta.worktreePath ?? '';\n if (recorded !== '' && worktreePath !== '' && path.resolve(recorded) !== path.resolve(worktreePath)) {\n return new AgentStateLookup('',\n `the harness records that agent against a different worktree (${recorded})`);\n }\n return new AgentStateLookup(path.join(subagents, `${agentId}.jsonl`), '');\n }\n return new AgentStateLookup('', 'the Claude Code harness has no state file for that agent id');\n }\n\n /** Every `<config>/projects/<slug>/<session>/subagents` directory that exists right now. */\n private subagentDirs(): string[] {\n const out: string[] = [];\n for (const root of this.projectsRoots()) {\n for (const project of this.readDir(root)) {\n const projectDir = path.join(root, project);\n for (const session of this.readDir(projectDir)) {\n const subagents = path.join(projectDir, session, 'subagents');\n if (this.isDirectory(subagents)) out.push(subagents);\n }\n }\n }\n return out;\n }\n\n /**\n * Seam: where the harness keeps its per-project state, delegated to the ONE resolver\n * ({@link ClaudeConfigDir}) so this file and the two provenance modules cannot disagree about\n * it — they did, and the divergence hard-blocked every PR on a relocated config dir. Specs point\n * `$CLAUDE_CONFIG_DIR` at a fixture tree, which exercises the same resolution the field uses.\n */\n protected projectsRoots(): string[] {\n return claudeConfigDir.projectsRoots();\n }\n\n private minutesAgo(now: number, written: number): string {\n const minutes = Math.max(0, Math.floor((now - written) / 60000));\n return minutes === 1 ? '1 minute ago' : `${String(minutes)} minutes ago`;\n }\n\n // ── Everything below is a guarded filesystem read: any failure is silence, never a throw. ──\n\n private exists(filePath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(filePath);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // null means \"it is there and this release could not make sense of it\" — NOT \"it is absent\".\n private readMeta(metaPath: string): RawAgentMeta | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(metaPath, 'utf8')) as RawAgentMeta;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * The last complete JSONL record of `transcript`, or null.\n *\n * Reads only the tail, because these files grow without bound and only the final line matters. A\n * window that starts mid-record yields an unparseable fragment and therefore null — \"cannot tell\"\n * — which is exactly what a truncated or half-written last line should also produce.\n */\n private lastRecord(transcript: string): RawTranscriptRecord | null {\n const lines = this.readTail(transcript).split('\\n')\n .filter((line: string): boolean => line.trim() !== '');\n if (lines.length === 0) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(lines[lines.length - 1]) as RawTranscriptRecord;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private readTail(filePath: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const size = fs.statSync(filePath).size;\n const length = Math.min(size, TRANSCRIPT_TAIL_BYTES);\n const buffer = Buffer.alloc(length);\n const fd = fs.openSync(filePath, 'r');\n fs.readSync(fd, buffer, 0, length, size - length);\n fs.closeSync(fd);\n return buffer.toString('utf8');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n private readDir(dirPath: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dirPath);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private isDirectory(dirPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(dirPath).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // 0 when the file is absent or unreadable — which reads as \"no evidence\", not \"written long ago\".\n private mtimeOf(filePath: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(filePath).mtimeMs;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 0;\n }\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export { ChecklistInstructionsService } from './checklist-instructions';
|
|
|
67
67
|
export { ReviewerInstructionsService, ReviewerBriefing, BriefedFile, ContextEntry, READ_TRUNCATION_LINES, ALL_DIFF_ONE_READ_LINES, } from './reviewer-instructions';
|
|
68
68
|
export { GateTokenService, computeGateToken, gateTokenMarker, extractGateToken, verifyGateToken, } from './gate-token';
|
|
69
69
|
export { SubagentProvenanceService, ReviewerEvidence, ReviewerContext, ExpectedReviewer, TranscriptScan, ProvenanceResult, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED, } from './subagent-provenance';
|
|
70
|
+
export { CLAUDE_CONFIG_DIR_ENV, ClaudeConfigDir, claudeConfigDir } from './claude-config-dir';
|
|
70
71
|
export { ReviewProvenanceService, ReviewProvenance, ReviewerTranscript, ReviewerPaths, OfferedContext, ProvenanceWriteRequest, DEFAULT_RETENTION_DAYS, } from './review-provenance';
|
|
71
72
|
export { ReviewJson, PrContext, ChecklistResult, ChecklistVerdict, ChecklistOverride, ChecklistOverrideService, checklistOverrideService, CK_PASS, CK_WARN, CK_OVERRIDDEN, CK_FAIL, CK_MISSING, CK_BAD_FORMAT, VERDICT_GREEN, VERDICT_YELLOW, VERDICT_RED, VERDICT_STATUSES, SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS, RequiredChecklist, ChecklistReviewContext, ReviewJsonService, prDirFor, reviewJsonPath, reviewJsonSchemaHint, } from './review-json';
|
|
72
73
|
export { MainSyncStatus, MainSyncStatusFile, MainSyncFileStore, PullRequestIndex, MAIN_SYNC_STATUS_VERSION, } from './main-sync-file';
|
|
@@ -83,3 +84,6 @@ export type { MutationVerb, MutationPhase } from './branch-mutation-log';
|
|
|
83
84
|
export { BranchMutationEvent, BranchMutationLog, branchMutationLogPath, logBranchMutation, } from './branch-mutation-log';
|
|
84
85
|
export { CommandsConfig, buildCommandsConfig, DEFAULT_UPSERT_PR_COMMAND, DEFAULT_MERGE_COMPLETE_COMMAND, } from './commands-config';
|
|
85
86
|
export { StaleBinRemoval, StaleBinSweeper, staleBinSweeper } from './stale-bin-sweep';
|
|
87
|
+
export { SpecTempDirs, specTempDirs } from './spec-temp-dirs';
|
|
88
|
+
export { RepoScratchDirs, SCRATCH_DIR_NAME } from './repo-scratch-dirs';
|
|
89
|
+
export { TmpScratchSweeper, TMP_SCRATCH_RETENTION_DAYS, TMP_SCRATCH_PREFIX } from './tmp-scratch-sweep';
|
package/src/index.js
CHANGED
|
@@ -5,10 +5,10 @@ exports.ConfigPruner = exports.retiredRuleFor = exports.retiredKeyErrorsIn = exp
|
|
|
5
5
|
exports.DEFAULT_MATCH_RULES = exports.renderMatchRuleMessage = exports.compileMatchRulePatterns = exports.isMatchRuleAllowedPath = exports.findMatchRuleViolations = exports.MatchRuleViolation = exports.MatchRuleConfig = exports.OrphanSweepReport = exports.OrphanDirSweeper = exports.TRASH_RETENTION_DAYS = exports.TRASH_MANIFEST_FILE = exports.TRASH_STATE_DIR = exports.OrphanSweepManifest = exports.OrphanSweepResult = exports.FailedOrphan = exports.ArchivedOrphan = exports.OrphanDirArchiver = exports.OrphanCandidate = exports.OrphanDirScanner = exports.MAX_ROW_BYTES = exports.BUILDS_LOG_GENERATIONS = exports.MAX_BUILDS_LOG_BYTES = exports.BUILD_DONE_FAIL = exports.BUILD_DONE_SUCCESS = exports.BUILD_START = exports.BUILDS_LOCK_FILE = exports.BUILDS_LOG_FILE = exports.RunningBuild = exports.BuildTermination = exports.BuildTicket = exports.BuildsLog = exports.DOCUMENTATION_KEYS = exports.HOME_KEY_AI_DOC = exports.HOME_KEY_DOC = exports.DEFAULT_MAX_CONCURRENT_BUILDS = exports.HOME_KEY_SINGLE_ROUND_REVIEW = exports.HOME_KEY_TURN_OFF_ALL_REVIEWERS = exports.HOME_KEY_MAX_CONCURRENT_BUILDS = exports.HOME_KEY_WHOLE_REPO_BUILD_GUARD = exports.HOME_KEY_ORPHAN_DIR_SWEEP = exports.HOME_EXPERIMENTAL_SECTION = exports.HOME_CONFIG_FILE = exports.HOME_CONFIG_DIR = exports.RETIRED_HOME_CONFIG_KEYS = exports.RetiredHomeConfigKey = exports.HomeConfigService = exports.HomeConfig = exports.validateChecklistDocs = exports.PrunedKey = exports.PruneResult = void 0;
|
|
6
6
|
exports.NoDestructureConfig = exports.PrismaConverterConfig = exports.PrismaValidateDtosConfig = exports.NoImplicitAnyConfig = exports.NoAnyUnknownConfig = exports.NoInlineTypeLiteralsConfig = exports.RequireReturnTypeConfig = exports.MaxFileLinesConfig = exports.MaxMethodLinesConfig = exports.WP_FINISH_PUSH_DEV = exports.WP_PUSH_DEV = exports.WP_FINISH_UPSERT_PR = exports.WP_START_UPSERT_PR = exports.WP_FINISH_UPDATE = exports.WP_START_UPDATE = exports.SyncFlowGuidance = exports.WebpiecesRulesConfig = exports.UPGRADE_SHIM_COMMAND = exports.PRUNE_UNKNOWN_COMMAND = exports.PUSH_DEV_STATE_FILE = exports.MERGE_EXPLANATION_FILE = exports.MERGE_IN_PROGRESS_FILE = exports.PR_REVIEW_DIR = exports.MERGE_INFO_DIR = exports.WEBPIECES_TMP_DIR = exports.hasDisable = exports.RULE_NAMES = exports.WEBPIECES_DISABLE = exports.AbstractRule = exports.ChangedFilesOptions = exports.DiffRange = exports.DiffScope = exports.isNewOrModified = exports.hasChangesInRange = exports.findNewMethodSignaturesInDiff = exports.getChangedLineNumbers = exports.getFileDiff = exports.getChangedFiles = exports.resolveBase = exports.detectBase = exports.SkipRuleResult = exports.getCurrentBranch = exports.shouldSkipRule = exports.FieldDef = exports.sectionForRule = exports.isHookGuard = exports.PR_LIFECYCLE_GUARD_KEY = exports.BRANCH_STATE_GUARD_KEY = exports.HOOK_GUARD_NAMES = exports.schemaFieldNames = void 0;
|
|
7
7
|
exports.LandPrConfig = exports.DEFAULT_BUILD_COMMAND = exports.PrGateConfig = exports.GateDefinition = exports.DEFAULT_MAX_COMMITS_BEHIND = exports.BranchStateGuardConfig = exports.DEFAULT_BANNED_STATE_PATH_PREFIXES = exports.DEFAULT_TEMPLATE_DIRS = exports.NoStatePathsInTemplatesConfig = exports.CLIENT_CREATION_SEVERITIES = exports.NoClientCreationOutsideServerOrClientConfig = exports.VALIDATE_TS_MODES = exports.STRUCTURAL_MODES = exports.ON_OFF_MODES = exports.THROW_CAUSE_MODES = exports.DIRECT_API_RESOLVER_MODES = exports.PRISMA_CONVERTER_MODES = exports.PRISMA_DTOS_MODES = exports.PROJECT_MODES = exports.MODIFIED_CODE_MODES = exports.INLINE_TYPE_MODES = exports.RETURN_TYPE_MODES = exports.FILE_LIMIT_MODES = exports.METHOD_LIMIT_MODES = exports.BaseRuleConfig = exports.ValidateEslintSyncConfig = exports.ValidateVersionsLockedConfig = exports.ValidatePackageJsonConfig = exports.ValidateNoArchitectureCyclesConfig = exports.ValidateArchitectureUnchangedConfig = exports.ValidateTsInSrcConfig = exports.NoJsFilesConfig = exports.DiGraphConfig = exports.NxWiringConfig = exports.RuntimeArchitectureConfig = exports.NoFileImportCyclesConfig = exports.PrLifecycleGuardConfig = exports.BranchCreationGuardConfig = exports.EnsureWeAreSecureConfig = exports.RoleTagConfig = exports.FrameworkTagConfig = exports.InjectAnnotationNotNeededForConcreteClassConfig = exports.NoFunctionOutsideClassConfig = exports.NoProcessExitOutsideMainConfig = exports.NoCustomCssConfig = exports.NoSymbolDiTokensConfig = exports.AngularNoDirectApiInResolverConfig = exports.ThrowCauseRequiredConfig = exports.CatchErrorPatternConfig = exports.NoUnmanagedExceptionsConfig = void 0;
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
11
|
-
exports.staleBinSweeper = exports.StaleBinSweeper = exports.StaleBinRemoval = exports.DEFAULT_MERGE_COMPLETE_COMMAND = exports.DEFAULT_UPSERT_PR_COMMAND = exports.buildCommandsConfig = exports.CommandsConfig = exports.logBranchMutation = exports.branchMutationLogPath = void 0;
|
|
8
|
+
exports.ReviewerPaths = exports.ReviewerTranscript = exports.ReviewProvenance = exports.ReviewProvenanceService = exports.claudeConfigDir = exports.ClaudeConfigDir = exports.CLAUDE_CONFIG_DIR_ENV = exports.PROVENANCE_SKIPPED = exports.PROVENANCE_MISSING = exports.PROVENANCE_OK = exports.ProvenanceResult = exports.TranscriptScan = exports.ExpectedReviewer = exports.ReviewerContext = exports.ReviewerEvidence = exports.SubagentProvenanceService = exports.verifyGateToken = exports.extractGateToken = exports.gateTokenMarker = exports.computeGateToken = exports.GateTokenService = exports.ALL_DIFF_ONE_READ_LINES = exports.READ_TRUNCATION_LINES = exports.ContextEntry = exports.BriefedFile = exports.ReviewerBriefing = exports.ReviewerInstructionsService = exports.ChecklistInstructionsService = exports.ChecklistValidator = exports.formatFileList = exports.normalizeChecklistDoc = exports.toChecklist = exports.DEFAULT_REVIEWER_AGENT_NAME = exports.REVIEWER_AGENTS_PLACEHOLDER = exports.ReviewerAgentPolicy = exports.ChecklistDefinition = exports.MERGE_MODES = exports.MERGE_MODE_NONE = exports.MERGE_MODE_AUTO = exports.buildDevDeployConfig = exports.buildLandPrConfig = exports.buildPrGateConfig = exports.defaultDevDeployConfig = exports.defaultLandPrConfig = exports.defaultPrGateConfig = exports.defaultGates = exports.ReviewContextEntry = exports.DEFAULT_DEV_BRANCH = exports.DEFAULT_DEV_BRANCH_NAMESPACE = exports.DevDeployConfig = void 0;
|
|
9
|
+
exports.computeMainSyncStatus = exports.finishedLock = exports.inProcessLock = exports.tryAcquireMainSyncLock = exports.isRefreshInProgress = exports.isLockStale = exports.writeMainSyncLock = exports.readMainSyncLock = exports.computeAllMainSyncStatuses = exports.writeMainSyncStatusFile = exports.writeMainSyncStatus = exports.readMainSyncStatusFile = exports.readMainSyncStatus = exports.mainSyncLockPath = exports.mainSyncStatusPath = exports.DEFAULT_HANG_TIMEOUT_MINUTES = exports.MainSyncStatusService = exports.MainSyncLock = exports.MAIN_SYNC_STATUS_VERSION = exports.PullRequestIndex = exports.MainSyncFileStore = exports.MainSyncStatusFile = exports.MainSyncStatus = exports.reviewJsonSchemaHint = exports.reviewJsonPath = exports.prDirFor = exports.ReviewJsonService = exports.ChecklistReviewContext = exports.RequiredChecklist = exports.SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS = exports.VERDICT_STATUSES = exports.VERDICT_RED = exports.VERDICT_YELLOW = exports.VERDICT_GREEN = exports.CK_BAD_FORMAT = exports.CK_MISSING = exports.CK_FAIL = exports.CK_OVERRIDDEN = exports.CK_WARN = exports.CK_PASS = exports.checklistOverrideService = exports.ChecklistOverrideService = exports.ChecklistOverride = exports.ChecklistVerdict = exports.ChecklistResult = exports.PrContext = exports.ReviewJson = exports.DEFAULT_RETENTION_DAYS = exports.ProvenanceWriteRequest = exports.OfferedContext = void 0;
|
|
10
|
+
exports.WorktreeReapResult = exports.ReapedWorktree = exports.BranchReaper = exports.ReapResult = exports.ReapedBranch = exports.HarnessAgentActivityReader = exports.AgentActivity = exports.AGENT_TRANSCRIPT_QUIET_MS = exports.AGENT_ACTIVITY_UNKNOWN = exports.AGENT_ACTIVITY_RETURNED = exports.AGENT_ACTIVITY_LIVE = exports.WorktreeLockVerdicts = exports.LockEvidence = exports.LockDecision = exports.LOCK_LIVENESS_UNVERIFIABLE = exports.HARNESS_NOT_CONSULTED = exports.AgentWorktreeLockReader = exports.AgentWorktreeLock = exports.WorktreeService = exports.WorktreeWorkInFlight = exports.Worktree = exports.BRANCH_RETENTION_KEEP = exports.BRANCH_RETENTION_ARCHIVE_TAG = exports.BRANCH_RETENTION_DELETE = exports.BRANCH_RETENTIONS = exports.ARCHIVE_TAG_PREFIX = exports.ArchiveResult = exports.BranchArchiver = exports.ADJUDICATED_CLASSIFICATIONS = exports.CLASSIFICATION_DETACHED = exports.CLASSIFICATION_CURRENT = exports.CLASSIFICATION_LOCKED = exports.CLASSIFICATION_PRUNABLE = exports.CLASSIFICATION_IN_USE = exports.CLASSIFICATION_NEVER_PROPOSED = exports.CLASSIFICATION_CONTENT_IN_MAIN = exports.CLASSIFICATION_SUPERSEDED = exports.CLASSIFICATION_NO_COMMITS = exports.CLASSIFICATION_BACKUP_OF_LIVE = exports.CLASSIFICATION_BACKUP_OF_MERGED = exports.CLASSIFICATION_MERGED_PR = exports.CACHE_STALE_AFTER_MS = exports.CacheFreshness = exports.MergedBranchesService = exports.MergedBranchesCache = exports.DeletableWorktree = exports.DeletableBranch = exports.MergedBranch = exports.squashRecoverySteps = exports.stampCleanMainSyncStatus = void 0;
|
|
11
|
+
exports.TMP_SCRATCH_PREFIX = exports.TMP_SCRATCH_RETENTION_DAYS = exports.TmpScratchSweeper = exports.SCRATCH_DIR_NAME = exports.RepoScratchDirs = exports.specTempDirs = exports.SpecTempDirs = exports.staleBinSweeper = exports.StaleBinSweeper = exports.StaleBinRemoval = exports.DEFAULT_MERGE_COMPLETE_COMMAND = exports.DEFAULT_UPSERT_PR_COMMAND = exports.buildCommandsConfig = exports.CommandsConfig = exports.logBranchMutation = exports.branchMutationLogPath = exports.BranchMutationLog = exports.BranchMutationEvent = exports.WorktreeReaper = void 0;
|
|
12
12
|
var types_1 = require("./types");
|
|
13
13
|
Object.defineProperty(exports, "ResolvedConfig", { enumerable: true, get: function () { return types_1.ResolvedConfig; } });
|
|
14
14
|
Object.defineProperty(exports, "ResolvedRuleConfig", { enumerable: true, get: function () { return types_1.ResolvedRuleConfig; } });
|
|
@@ -410,6 +410,10 @@ Object.defineProperty(exports, "ProvenanceResult", { enumerable: true, get: func
|
|
|
410
410
|
Object.defineProperty(exports, "PROVENANCE_OK", { enumerable: true, get: function () { return subagent_provenance_1.PROVENANCE_OK; } });
|
|
411
411
|
Object.defineProperty(exports, "PROVENANCE_MISSING", { enumerable: true, get: function () { return subagent_provenance_1.PROVENANCE_MISSING; } });
|
|
412
412
|
Object.defineProperty(exports, "PROVENANCE_SKIPPED", { enumerable: true, get: function () { return subagent_provenance_1.PROVENANCE_SKIPPED; } });
|
|
413
|
+
var claude_config_dir_1 = require("./claude-config-dir");
|
|
414
|
+
Object.defineProperty(exports, "CLAUDE_CONFIG_DIR_ENV", { enumerable: true, get: function () { return claude_config_dir_1.CLAUDE_CONFIG_DIR_ENV; } });
|
|
415
|
+
Object.defineProperty(exports, "ClaudeConfigDir", { enumerable: true, get: function () { return claude_config_dir_1.ClaudeConfigDir; } });
|
|
416
|
+
Object.defineProperty(exports, "claudeConfigDir", { enumerable: true, get: function () { return claude_config_dir_1.claudeConfigDir; } });
|
|
413
417
|
var review_provenance_1 = require("./review-provenance");
|
|
414
418
|
Object.defineProperty(exports, "ReviewProvenanceService", { enumerable: true, get: function () { return review_provenance_1.ReviewProvenanceService; } });
|
|
415
419
|
Object.defineProperty(exports, "ReviewProvenance", { enumerable: true, get: function () { return review_provenance_1.ReviewProvenance; } });
|
|
@@ -541,4 +545,14 @@ var stale_bin_sweep_1 = require("./stale-bin-sweep");
|
|
|
541
545
|
Object.defineProperty(exports, "StaleBinRemoval", { enumerable: true, get: function () { return stale_bin_sweep_1.StaleBinRemoval; } });
|
|
542
546
|
Object.defineProperty(exports, "StaleBinSweeper", { enumerable: true, get: function () { return stale_bin_sweep_1.StaleBinSweeper; } });
|
|
543
547
|
Object.defineProperty(exports, "staleBinSweeper", { enumerable: true, get: function () { return stale_bin_sweep_1.staleBinSweeper; } });
|
|
548
|
+
var spec_temp_dirs_1 = require("./spec-temp-dirs");
|
|
549
|
+
Object.defineProperty(exports, "SpecTempDirs", { enumerable: true, get: function () { return spec_temp_dirs_1.SpecTempDirs; } });
|
|
550
|
+
Object.defineProperty(exports, "specTempDirs", { enumerable: true, get: function () { return spec_temp_dirs_1.specTempDirs; } });
|
|
551
|
+
var repo_scratch_dirs_1 = require("./repo-scratch-dirs");
|
|
552
|
+
Object.defineProperty(exports, "RepoScratchDirs", { enumerable: true, get: function () { return repo_scratch_dirs_1.RepoScratchDirs; } });
|
|
553
|
+
Object.defineProperty(exports, "SCRATCH_DIR_NAME", { enumerable: true, get: function () { return repo_scratch_dirs_1.SCRATCH_DIR_NAME; } });
|
|
554
|
+
var tmp_scratch_sweep_1 = require("./tmp-scratch-sweep");
|
|
555
|
+
Object.defineProperty(exports, "TmpScratchSweeper", { enumerable: true, get: function () { return tmp_scratch_sweep_1.TmpScratchSweeper; } });
|
|
556
|
+
Object.defineProperty(exports, "TMP_SCRATCH_RETENTION_DAYS", { enumerable: true, get: function () { return tmp_scratch_sweep_1.TMP_SCRATCH_RETENTION_DAYS; } });
|
|
557
|
+
Object.defineProperty(exports, "TMP_SCRATCH_PREFIX", { enumerable: true, get: function () { return tmp_scratch_sweep_1.TMP_SCRATCH_PREFIX; } });
|
|
544
558
|
//# sourceMappingURL=index.js.map
|