@webpieces/repo-workflow-core 0.4.849 → 0.4.850
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/repo-workflow-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.850",
|
|
4
4
|
"description": "Shared repository branch, worktree, sync, and identity-stamp mechanics.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"inversify": "7.10.4",
|
|
23
23
|
"reflect-metadata": "0.2.2",
|
|
24
|
-
"@webpieces/tooling-common": "0.4.
|
|
24
|
+
"@webpieces/tooling-common": "0.4.850",
|
|
25
25
|
"tslib": "2.8.1"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
@@ -44,7 +44,7 @@ export declare class ReviewIdentityStamp {
|
|
|
44
44
|
* Writes (from the PreToolUse hook) and consumes (from `wp-write-review`) {@link ReviewIdentityStamp}s.
|
|
45
45
|
*
|
|
46
46
|
* Both ends resolve the path from their OWN working directory through {@link DotWebpieces.local}, which is
|
|
47
|
-
* keyed by git's answer for that tree — so the hook's
|
|
47
|
+
* keyed by git's answer for that tree — so the hook's effective command cwd and the bin's process cwd land on the same
|
|
48
48
|
* file for the same worktree, and two worktrees never share a stamp.
|
|
49
49
|
*
|
|
50
50
|
* `@injectable(bindingScopeValues.Singleton)` so it is injected by type; {@link reviewIdentityStamps} is the
|
|
@@ -61,7 +61,7 @@ exports.ReviewIdentityStamp = ReviewIdentityStamp;
|
|
|
61
61
|
* Writes (from the PreToolUse hook) and consumes (from `wp-write-review`) {@link ReviewIdentityStamp}s.
|
|
62
62
|
*
|
|
63
63
|
* Both ends resolve the path from their OWN working directory through {@link DotWebpieces.local}, which is
|
|
64
|
-
* keyed by git's answer for that tree — so the hook's
|
|
64
|
+
* keyed by git's answer for that tree — so the hook's effective command cwd and the bin's process cwd land on the same
|
|
65
65
|
* file for the same worktree, and two worktrees never share a stamp.
|
|
66
66
|
*
|
|
67
67
|
* `@injectable(bindingScopeValues.Singleton)` so it is injected by type; {@link reviewIdentityStamps} is the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-identity-stamp.js","sourceRoot":"","sources":["../../../../../packages/tooling/repo-workflow-core/src/review-identity-stamp.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,yCAA2D;AAC3D,8DAAuD;AACvD,8DAAuE;AACvE,iEAA6D;AAE7D,kFAAkF;AACrE,QAAA,gBAAgB,GAAG,iBAAiB,CAAC;AAElD,8EAA8E;AACjE,QAAA,2BAA2B,GAAG,aAAa,CAAC;AAEzD,0GAA0G;AAC7F,QAAA,gBAAgB,GAAG,eAAe,CAAC;AAEhD;;;;GAIG;AACU,QAAA,uBAAuB,GAAG,OAAO,CAAC;AAE/C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAa,mBAAmB;IAC5B,WAAW,CAAS;IACpB,MAAM,CAAS,CAAO,oEAAoE;IAC1F,SAAS,CAAS;IAClB,OAAO,CAAS,CAAM,kDAAkD;IACxE,SAAS,CAAS;IAClB,GAAG,CAAS;IACZ,SAAS,CAAS,CAAI,MAAM;IAE5B,yDAAyD;IACzD,YAAY,WAAmB,EAAE,MAAc,EAAE,SAAiB,EAAE,OAAe,EAAE,SAAiB,EAAE,GAAW,EAAE,SAAiB;QAClI,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAnBD,kDAmBC;AAED;;;;;;;;;GASG;AAEI,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IAEd;IACA;IAFrB,YACqB,SAAuB,6BAAY,EACnC,aAAyB,IAAI,2BAAU,EAAE;QADzC,WAAM,GAAN,MAAM,CAA6B;QACnC,eAAU,GAAV,UAAU,CAA+B;IAC3D,CAAC;IAEJ;;;;OAIG;IACH,iBAAiB,CAAC,OAAe;QAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,wBAAgB,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QACtE,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,iDAAiD,CAAC;QAC/D,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,OAAO,KAAK,KAAK,IAAI,EAAE,CAAC;YACpB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,KAA0B;QACpC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5E,CAAC;IAED,SAAS,CAAC,QAAgB,EAAE,WAAmB;QAC3C,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,wBAAgB,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACrH,CAAC;IAED,qGAAqG;IACrG,KAAK,CAAC,QAAgB,EAAE,KAA0B;QAC9C,yHAAyH;QACzH,8DAA8D;QAC9D,IAAI,CAAC;YACD,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,CAAC;QACxF,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;QACf,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,QAAgB,EAAE,WAAmB,EAAE,QAAgB,IAAI,CAAC,GAAG,EAAE;QAClE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAClB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,WAAW,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QACrE,MAAM,GAAG,GAAG,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,+BAAuB;YAAE,OAAO,IAAI,CAAC;QACnF,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,IAAI,CAAC,IAAY;QACrB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACtC,2FAA2F;QAC3F,8DAA8D;QAC9D,IAAI,CAAC;YACD,wFAAwF;YACxF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA4B,CAAC;YACjF,kFAAkF;YAClF,MAAM,GAAG,GAAG,CAAC,CAAU,EAAU,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACrE,OAAO,IAAI,mBAAmB,CAC1B,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EACvF,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACvE,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,IAAY;QACvB,wGAAwG;QACxG,8DAA8D;QAC9D,IAAI,CAAC;YACD,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;QACf,CAAC;IACL,CAAC;CACJ,CAAA;AA5FY,gEAA0B;qCAA1B,0BAA0B;IADtC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGR,6BAAY;QACR,2BAAU;GAHlC,0BAA0B,CA4FtC;AAED,8FAA8F;AACjF,QAAA,oBAAoB,GAAG,IAAI,0BAA0B,EAAE,CAAC","sourcesContent":["import * as fs from 'fs';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AtomicFile } from '@webpieces/tooling-common';\nimport { DotWebpieces, dotWebpieces } from '@webpieces/tooling-common';\nimport { toError } from '@webpieces/tooling-common/to-error';\n\n/** The ONE sanctioned way a reviewer submits a checklist verdict (issue #863). */\nexport const WRITE_REVIEW_BIN = 'wp-write-review';\n\n/** The flag that names the checklist a `wp-write-review` call submits for. */\nexport const WRITE_REVIEW_CHECKLIST_FLAG = '--checklist';\n\n/** Where the stamps live, beneath this worktree's tooling-owned state dir (never the AI-writable one). */\nexport const REVIEW_STAMP_DIR = 'review-stamps';\n\n/**\n * How long a stamp stays usable. The hook writes it a few milliseconds before the harness starts the very\n * Bash command it stamped, so anything older than this is left over from a call that never reached the bin\n * — and must not be available to lend its identity to a later caller.\n */\nexport const REVIEW_STAMP_MAX_AGE_MS = 120_000;\n\n/**\n * WHO is running a `pnpm wp-write-review` Bash call, as the HARNESS reported it to the PreToolUse hook.\n * Data-only (per CLAUDE.md), and its field names are the JSON keys on disk.\n *\n * WHY a hook stamp and not the environment (issue #863). A Bash-invoked bin sees only its env, and the env\n * cannot answer \"is this a subagent?\" in either harness:\n * - Claude Code exports `CLAUDE_CODE_SESSION_ID`, which is the PARENT session's id in a subagent too, and\n * no agent id at all — a reviewer and its coordinator have byte-identical identity env.\n * - Codex exports `CODEX_SESSION_ID` / `CODEX_THREAD_ID`, and nothing documents (or has been measured to\n * show) that a spawned subagent gets a thread id of its own rather than inheriting the coordinator's.\n * The hook payload DOES carry the answer, in both harnesses, on the same key: `agent_id` is empty for the\n * main loop and populated for a subagent. So the hook — which sees every Bash call before it runs — writes\n * that identity here for each checklist the command names, and the bin consumes it.\n *\n * NOT tamper-proof, and not claimed to be: an agent can write this file by hand. It moves forging a verdict\n * from \"write one JSON file\" (what a coordinator did on two merged PRs) to a deliberate, auditable forgery of\n * tooling state outside the AI-writable dir — the same bar SubagentProvenanceService sets.\n */\nexport class ReviewIdentityStamp {\n checklistId: string;\n aiType: string; // 'claude-code' | 'codex' — the adapter's answer, never the model's\n sessionId: string;\n agentId: string; // '' ⇒ the MAIN loop, i.e. the coordinating agent\n agentType: string;\n cwd: string;\n stampedAt: string; // ISO\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(checklistId: string, aiType: string, sessionId: string, agentId: string, agentType: string, cwd: string, stampedAt: string) {\n this.checklistId = checklistId;\n this.aiType = aiType;\n this.sessionId = sessionId;\n this.agentId = agentId;\n this.agentType = agentType;\n this.cwd = cwd;\n this.stampedAt = stampedAt;\n }\n}\n\n/**\n * Writes (from the PreToolUse hook) and consumes (from `wp-write-review`) {@link ReviewIdentityStamp}s.\n *\n * Both ends resolve the path from their OWN working directory through {@link DotWebpieces.local}, which is\n * keyed by git's answer for that tree — so the hook's payload cwd and the bin's process cwd land on the same\n * file for the same worktree, and two worktrees never share a stamp.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type; {@link reviewIdentityStamps} is the\n * instance the dependency-free hook adapter uses.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewIdentityStampService {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n ) {}\n\n /**\n * The checklist ids a Bash command submits verdicts for, or [] when it does not invoke the bin at all.\n * Every `--checklist <id>` / `--checklist=<id>` is returned, because one reviewer covering several\n * checklists may chain several submissions into one command.\n */\n invokedChecklists(command: string): string[] {\n if (!new RegExp(`\\\\b${WRITE_REVIEW_BIN}\\\\b`).test(command)) return [];\n const ids: string[] = [];\n const flag = /--checklist(?:=|\\s+)([\"']?)([A-Za-z0-9._-]+)\\1/g;\n let match = flag.exec(command);\n while (match !== null) {\n if (!ids.includes(match[2])) ids.push(match[2]);\n match = flag.exec(command);\n }\n return ids;\n }\n\n /**\n * True when the stamp names the COORDINATING agent rather than a subagent: no agent id, or an agent id\n * equal to the session id (a harness that fills the main loop's agent id with the session id instead of\n * leaving it empty). The coordinator may never submit a reviewer's verdict — that is the whole point.\n */\n isCoordinator(stamp: ReviewIdentityStamp): boolean {\n return stamp.agentId.trim() === '' || stamp.agentId === stamp.sessionId;\n }\n\n stampPath(startDir: string, checklistId: string): string {\n return this.dotDir.localFile(startDir, REVIEW_STAMP_DIR, `${checklistId.replace(/[^A-Za-z0-9._-]/g, '_')}.json`);\n }\n\n /** Best-effort: a stamp that cannot be written makes the bin refuse, which is the safe direction. */\n write(startDir: string, stamp: ReviewIdentityStamp): void {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the hook must never fail a tool call over its own bookkeeping\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n this.atomicFile.writeJsonAtomic(this.stampPath(startDir, stamp.checklistId), stamp);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Read AND delete the stamp for one checklist. null when there is none, it is unreadable, or it is\n * older than {@link REVIEW_STAMP_MAX_AGE_MS}. Deleted on read so one hook call vouches for one\n * submission; a delete that fails (a sandbox that denies it) is harmless, because age bounds it anyway.\n */\n take(startDir: string, checklistId: string, nowMs: number = Date.now()): ReviewIdentityStamp | null {\n const file = this.stampPath(startDir, checklistId);\n const stamp = this.read(file);\n this.remove(file);\n if (stamp === null || stamp.checklistId !== checklistId) return null;\n const age = nowMs - Date.parse(stamp.stampedAt);\n if (!Number.isFinite(age) || age < 0 || age > REVIEW_STAMP_MAX_AGE_MS) return null;\n return stamp;\n }\n\n private read(file: string): ReviewIdentityStamp | null {\n if (!fs.existsSync(file)) return null;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable stamp is no stamp\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field below\n const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;\n // webpieces-disable no-any-unknown -- one opaque JSON value, narrowed to a string\n const str = (v: unknown): string => (typeof v === 'string' ? v : '');\n return new ReviewIdentityStamp(\n str(raw['checklistId']), str(raw['aiType']), str(raw['sessionId']), str(raw['agentId']),\n str(raw['agentType']), str(raw['cwd']), str(raw['stampedAt']));\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private remove(file: string): void {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a stamp that cannot be deleted still expires\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(file, { force: true });\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n/** The instance the hook adapter uses — it runs without a DI container (see hook-core.ts). */\nexport const reviewIdentityStamps = new ReviewIdentityStampService();\n"]}
|
|
1
|
+
{"version":3,"file":"review-identity-stamp.js","sourceRoot":"","sources":["../../../../../packages/tooling/repo-workflow-core/src/review-identity-stamp.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AtomicFile } from '@webpieces/tooling-common';\nimport { DotWebpieces, dotWebpieces } from '@webpieces/tooling-common';\nimport { toError } from '@webpieces/tooling-common/to-error';\n\n/** The ONE sanctioned way a reviewer submits a checklist verdict (issue #863). */\nexport const WRITE_REVIEW_BIN = 'wp-write-review';\n\n/** The flag that names the checklist a `wp-write-review` call submits for. */\nexport const WRITE_REVIEW_CHECKLIST_FLAG = '--checklist';\n\n/** Where the stamps live, beneath this worktree's tooling-owned state dir (never the AI-writable one). */\nexport const REVIEW_STAMP_DIR = 'review-stamps';\n\n/**\n * How long a stamp stays usable. The hook writes it a few milliseconds before the harness starts the very\n * Bash command it stamped, so anything older than this is left over from a call that never reached the bin\n * — and must not be available to lend its identity to a later caller.\n */\nexport const REVIEW_STAMP_MAX_AGE_MS = 120_000;\n\n/**\n * WHO is running a `pnpm wp-write-review` Bash call, as the HARNESS reported it to the PreToolUse hook.\n * Data-only (per CLAUDE.md), and its field names are the JSON keys on disk.\n *\n * WHY a hook stamp and not the environment (issue #863). A Bash-invoked bin sees only its env, and the env\n * cannot answer \"is this a subagent?\" in either harness:\n * - Claude Code exports `CLAUDE_CODE_SESSION_ID`, which is the PARENT session's id in a subagent too, and\n * no agent id at all — a reviewer and its coordinator have byte-identical identity env.\n * - Codex exports `CODEX_SESSION_ID` / `CODEX_THREAD_ID`, and nothing documents (or has been measured to\n * show) that a spawned subagent gets a thread id of its own rather than inheriting the coordinator's.\n * The hook payload DOES carry the answer, in both harnesses, on the same key: `agent_id` is empty for the\n * main loop and populated for a subagent. So the hook — which sees every Bash call before it runs — writes\n * that identity here for each checklist the command names, and the bin consumes it.\n *\n * NOT tamper-proof, and not claimed to be: an agent can write this file by hand. It moves forging a verdict\n * from \"write one JSON file\" (what a coordinator did on two merged PRs) to a deliberate, auditable forgery of\n * tooling state outside the AI-writable dir — the same bar SubagentProvenanceService sets.\n */\nexport class ReviewIdentityStamp {\n checklistId: string;\n aiType: string; // 'claude-code' | 'codex' — the adapter's answer, never the model's\n sessionId: string;\n agentId: string; // '' ⇒ the MAIN loop, i.e. the coordinating agent\n agentType: string;\n cwd: string;\n stampedAt: string; // ISO\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(checklistId: string, aiType: string, sessionId: string, agentId: string, agentType: string, cwd: string, stampedAt: string) {\n this.checklistId = checklistId;\n this.aiType = aiType;\n this.sessionId = sessionId;\n this.agentId = agentId;\n this.agentType = agentType;\n this.cwd = cwd;\n this.stampedAt = stampedAt;\n }\n}\n\n/**\n * Writes (from the PreToolUse hook) and consumes (from `wp-write-review`) {@link ReviewIdentityStamp}s.\n *\n * Both ends resolve the path from their OWN working directory through {@link DotWebpieces.local}, which is\n * keyed by git's answer for that tree — so the hook's effective command cwd and the bin's process cwd land on the same\n * file for the same worktree, and two worktrees never share a stamp.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type; {@link reviewIdentityStamps} is the\n * instance the dependency-free hook adapter uses.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewIdentityStampService {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n ) {}\n\n /**\n * The checklist ids a Bash command submits verdicts for, or [] when it does not invoke the bin at all.\n * Every `--checklist <id>` / `--checklist=<id>` is returned, because one reviewer covering several\n * checklists may chain several submissions into one command.\n */\n invokedChecklists(command: string): string[] {\n if (!new RegExp(`\\\\b${WRITE_REVIEW_BIN}\\\\b`).test(command)) return [];\n const ids: string[] = [];\n const flag = /--checklist(?:=|\\s+)([\"']?)([A-Za-z0-9._-]+)\\1/g;\n let match = flag.exec(command);\n while (match !== null) {\n if (!ids.includes(match[2])) ids.push(match[2]);\n match = flag.exec(command);\n }\n return ids;\n }\n\n /**\n * True when the stamp names the COORDINATING agent rather than a subagent: no agent id, or an agent id\n * equal to the session id (a harness that fills the main loop's agent id with the session id instead of\n * leaving it empty). The coordinator may never submit a reviewer's verdict — that is the whole point.\n */\n isCoordinator(stamp: ReviewIdentityStamp): boolean {\n return stamp.agentId.trim() === '' || stamp.agentId === stamp.sessionId;\n }\n\n stampPath(startDir: string, checklistId: string): string {\n return this.dotDir.localFile(startDir, REVIEW_STAMP_DIR, `${checklistId.replace(/[^A-Za-z0-9._-]/g, '_')}.json`);\n }\n\n /** Best-effort: a stamp that cannot be written makes the bin refuse, which is the safe direction. */\n write(startDir: string, stamp: ReviewIdentityStamp): void {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the hook must never fail a tool call over its own bookkeeping\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n this.atomicFile.writeJsonAtomic(this.stampPath(startDir, stamp.checklistId), stamp);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Read AND delete the stamp for one checklist. null when there is none, it is unreadable, or it is\n * older than {@link REVIEW_STAMP_MAX_AGE_MS}. Deleted on read so one hook call vouches for one\n * submission; a delete that fails (a sandbox that denies it) is harmless, because age bounds it anyway.\n */\n take(startDir: string, checklistId: string, nowMs: number = Date.now()): ReviewIdentityStamp | null {\n const file = this.stampPath(startDir, checklistId);\n const stamp = this.read(file);\n this.remove(file);\n if (stamp === null || stamp.checklistId !== checklistId) return null;\n const age = nowMs - Date.parse(stamp.stampedAt);\n if (!Number.isFinite(age) || age < 0 || age > REVIEW_STAMP_MAX_AGE_MS) return null;\n return stamp;\n }\n\n private read(file: string): ReviewIdentityStamp | null {\n if (!fs.existsSync(file)) return null;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable stamp is no stamp\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field below\n const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;\n // webpieces-disable no-any-unknown -- one opaque JSON value, narrowed to a string\n const str = (v: unknown): string => (typeof v === 'string' ? v : '');\n return new ReviewIdentityStamp(\n str(raw['checklistId']), str(raw['aiType']), str(raw['sessionId']), str(raw['agentId']),\n str(raw['agentType']), str(raw['cwd']), str(raw['stampedAt']));\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private remove(file: string): void {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a stamp that cannot be deleted still expires\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(file, { force: true });\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n/** The instance the hook adapter uses — it runs without a DI container (see hook-core.ts). */\nexport const reviewIdentityStamps = new ReviewIdentityStampService();\n"]}
|