@orchestrator-claude/definitions 3.36.0 → 3.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/agents/adversarial-reviewer.md +6 -1
  2. package/agents/api-extractor.md +6 -0
  3. package/agents/azure-devops-engineer.md +1 -2
  4. package/agents/business-rule-miner.md +6 -0
  5. package/agents/cicd-platform-engineer.md +1 -2
  6. package/agents/ciclo-engineer.md +15 -2
  7. package/agents/code-archaeologist.md +6 -0
  8. package/agents/debug-sidecar.md +6 -0
  9. package/agents/doc-sidecar.md +6 -0
  10. package/agents/docs-guardian.md +9 -3
  11. package/agents/frontend-engineer.md +1 -2
  12. package/agents/{ai-architect.md → harness-architect.md} +26 -5
  13. package/agents/implementer.md +6 -1
  14. package/agents/legacy-discoverer.md +6 -0
  15. package/agents/legacy-synthesizer.md +6 -0
  16. package/agents/methodology-researcher.md +6 -1
  17. package/agents/operational-artifact-builder.md +6 -1
  18. package/agents/orchestrator.md +27 -87
  19. package/agents/planner.md +6 -1
  20. package/agents/presentation-engineer.md +5 -4
  21. package/agents/python-data-engineer.md +0 -1
  22. package/agents/researcher.md +6 -0
  23. package/agents/reviewer.md +6 -1
  24. package/agents/schema-extractor.md +6 -0
  25. package/agents/software-architect.md +30 -7
  26. package/agents/specifier.md +6 -1
  27. package/agents/task-generator.md +6 -1
  28. package/agents/test-sidecar.md +6 -0
  29. package/dist/scripts/generate-hooks-matcher.d.ts +106 -0
  30. package/dist/scripts/generate-hooks-matcher.d.ts.map +1 -0
  31. package/dist/scripts/generate-hooks-matcher.js +153 -0
  32. package/dist/scripts/generate-hooks-matcher.js.map +1 -0
  33. package/dist/scripts/generate-manifest.d.ts +22 -0
  34. package/dist/scripts/generate-manifest.d.ts.map +1 -1
  35. package/dist/scripts/generate-manifest.js +67 -2
  36. package/dist/scripts/generate-manifest.js.map +1 -1
  37. package/hooks/agent-git-guard.mjs +63 -11
  38. package/hooks/dangling-guard.mjs +42 -12
  39. package/hooks/gate-guardian.mjs +38 -8
  40. package/hooks/hooks.json +3 -9
  41. package/hooks/lib/api-client.ts +128 -9
  42. package/hooks/lib/capability-scanner.ts +391 -55
  43. package/hooks/lib/dispatch-marker.ts +214 -0
  44. package/hooks/lib/git-checkpoint.ts +34 -37
  45. package/hooks/lib/observations-helper.ts +14 -1
  46. package/hooks/mailbox-listener.mjs +69 -17
  47. package/hooks/playwright-output-guard.mjs +40 -10
  48. package/hooks/post-implement.mjs +38 -8
  49. package/hooks/pre-agent-dispatch.mjs +137 -16
  50. package/hooks/pre-agent-dispatch.ts +82 -15
  51. package/hooks/session-start.mjs +274 -103
  52. package/hooks/session-start.ts +250 -69
  53. package/hooks/subagent-start.mjs +7665 -22
  54. package/hooks/subagent-start.ts +367 -16
  55. package/hooks/subagent-stop.mjs +102 -62
  56. package/hooks/subagent-stop.ts +124 -28
  57. package/hooks/user-prompt.mjs +43 -13
  58. package/hooks/verification-before-completion.mjs +63 -11
  59. package/hooks/workflow-guard.mjs +45 -11
  60. package/hooks-settings-template.json +1 -7
  61. package/manifest.json +62 -62
  62. package/package.json +4 -2
  63. package/skills/checkpoint-protocol/SKILL.md +19 -4
  64. package/skills/ciclo-lane-dispatch/SKILL.md +24 -0
  65. package/skills/fase-1b-projecao/SKILL.md +146 -0
  66. package/skills/orchestrator/SKILL.md +11 -3
  67. package/skills/workflow-coordination/SKILL.md +3 -3
  68. package/hooks/post-compact.mjs +0 -7931
  69. package/hooks/post-compact.ts +0 -170
@@ -0,0 +1,214 @@
1
+ /**
2
+ * dispatch-marker.ts — boundary marker per git ref (S445, RFC-042).
3
+ *
4
+ * WHAT THIS IS. At the instant a committing agent is dispatched, record WHERE
5
+ * HEAD WAS, as a git ref pointing at the full 40-hex sha. That is the whole
6
+ * feature: a COORDINATE for "the state before this agent started".
7
+ *
8
+ * WHAT THIS IS NOT, deliberately. It does not restore anything. RFC-042 settled
9
+ * that a checkpoint is an AUDIT MARK (D1) and is EXPLICIT (D2); asked whether a
10
+ * restore path was wanted, the answer was that rollback has been needed only a
11
+ * few times and plain git handled it. So rollback stays plain git:
12
+ *
13
+ * git reset --hard <ref>
14
+ *
15
+ * No restore code lives here. An unused restore path is precisely how the
16
+ * existing checkpoint subsystem accumulated four independent breaks (see the
17
+ * checkpoint-protocol skill) — a coordinate that git can consume cannot rot the
18
+ * same way, because git is the thing that consumes it.
19
+ *
20
+ * IT DOES NOT CREATE A COMMIT AND DOES NOT TOUCH THE WORKING TREE. A ref is git
21
+ * metadata: writing one leaves `git status` byte-identical. That is what lets
22
+ * this coexist with the S445 rule that ordinary machine operation must never
23
+ * write a git-tracked file.
24
+ *
25
+ * THE COLON TRAP (the reason sanitization is a requirement, not a nicety).
26
+ * Under plugin scope agents are named `orchestrator:implementer`. A git ref name
27
+ * may not contain a colon (nor space, tilde, caret, question mark, asterisk,
28
+ * open bracket, backslash, or a double dot). Passing a raw agent name through
29
+ * would make `update-ref` fail — and since the calling hook is fail-open, it
30
+ * would fail SILENTLY, leaving the whole feature a green no-op for exactly the
31
+ * agents that can commit. Hence sanitizeRefComponent, and hence a test that
32
+ * validates the output with git's own `check-ref-format`.
33
+ *
34
+ * RETENTION. Refs are GC roots, so markers would accumulate forever and we would
35
+ * have traded collectable orphan objects for uncollectable refs. Hence a cap.
36
+ */
37
+
38
+ import { execFileSync } from 'node:child_process';
39
+
40
+ /**
41
+ * Namespace for dispatch boundary markers. Verified free before adoption:
42
+ * `git for-each-ref refs/orchestrator/` returned 0 refs. The 6 pre-existing
43
+ * `orchestrator/*` TAGS live under `refs/tags/orchestrator/`, which is a
44
+ * DIFFERENT namespace — these markers do not collide with them, and are not
45
+ * tags (a tag is a published, human-meaningful mark; this is machine bookkeeping
46
+ * with a retention cap).
47
+ */
48
+ export const DISPATCH_MARKER_REF_PREFIX = 'refs/orchestrator/dispatch';
49
+
50
+ /**
51
+ * Maximum markers retained PER KEY (per branch — see writeDispatchBoundaryMarker
52
+ * for why branch and not workflow). Oldest are pruned once the cap is exceeded.
53
+ *
54
+ * WHY A CAP EXISTS AT ALL: a git ref is a GC ROOT. Every marker pins the commit
55
+ * it points at, so without pruning these would accumulate without bound and
56
+ * would keep otherwise-unreachable objects alive forever — trading collectable
57
+ * orphans for uncollectable refs, which is a worse deal than the problem being
58
+ * solved. 20 is deliberately small: markers are a rollback coordinate for RECENT
59
+ * work, and a coordinate 20 dispatches old has been superseded by ordinary
60
+ * commits and branches. Raise it only against a measured need.
61
+ */
62
+ export const MAX_DISPATCH_MARKERS_PER_KEY = 20;
63
+
64
+ /** Fallback when an input sanitizes down to nothing. */
65
+ const EMPTY_COMPONENT_PLACEHOLDER = 'unknown';
66
+
67
+ /**
68
+ * Reduces arbitrary text to a single ref-name component that git accepts.
69
+ *
70
+ * The allowlist is deliberately narrower than git's own rules rather than a
71
+ * blocklist of forbidden characters. Permitting only [A-Za-z0-9_-] makes whole
72
+ * FAMILIES of git's restrictions unreachable instead of individually handled:
73
+ * dropping '.' rules out a leading dot, a trailing dot, a '..' sequence and a
74
+ * '.lock' suffix at once; dropping '@' and '{' rules out '@{' and the lone '@'.
75
+ * A blocklist would have to enumerate each rule correctly and stay correct as
76
+ * git evolves; this cannot fail open by omission.
77
+ */
78
+ export function sanitizeRefComponent(raw: string): string {
79
+ const cleaned = (raw || '')
80
+ .replace(/[^A-Za-z0-9_-]/g, '-')
81
+ .replace(/-+/g, '-')
82
+ .replace(/^-+|-+$/g, '');
83
+
84
+ return cleaned || EMPTY_COMPONENT_PLACEHOLDER;
85
+ }
86
+
87
+ /**
88
+ * Compact UTC stamp, e.g. `20260804T142439123Z`. Colons are stripped because a
89
+ * plain ISO-8601 string (`2026-08-04T14:24:39.123Z`) is an INVALID ref name for
90
+ * the very same reason the agent name is. Fixed width and zero-padded, so
91
+ * lexicographic ref order equals chronological order — which is what makes
92
+ * "prune the oldest" a plain sort.
93
+ */
94
+ export function formatMarkerStamp(d: Date): string {
95
+ return d.toISOString().replace(/[-:.]/g, '');
96
+ }
97
+
98
+ /** `refs/orchestrator/dispatch/<branch>/<stamp>-<agent>`. */
99
+ export function buildMarkerRefName(branch: string, agent: string, stamp: string): string {
100
+ const branchPart = sanitizeRefComponent(branch);
101
+ const agentPart = sanitizeRefComponent(agent);
102
+ const stampPart = sanitizeRefComponent(stamp);
103
+
104
+ return `${DISPATCH_MARKER_REF_PREFIX}/${branchPart}/${stampPart}-${agentPart}`;
105
+ }
106
+
107
+ export type DispatchMarkerOutcome =
108
+ | { status: 'written'; ref: string; sha: string; pruned: string[] }
109
+ | { status: 'failed'; reason: string };
110
+
111
+ export interface DispatchMarkerDeps {
112
+ /** Runs git and returns stdout. MUST throw on non-zero exit. */
113
+ runGit: (args: string[]) => string;
114
+ /** Injectable clock (tests need deterministic, ordered stamps). */
115
+ now?: () => Date;
116
+ }
117
+
118
+ /** Production git runner: fixed argv, no shell, so no interpolation surface. */
119
+ export function runGitDefault(args: string[]): string {
120
+ return execFileSync('git', args, {
121
+ encoding: 'utf-8',
122
+ stdio: ['ignore', 'pipe', 'ignore'],
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Removes the oldest markers under `prefix` until at most `cap` remain.
128
+ * Lexicographic ref order is chronological (see formatMarkerStamp).
129
+ */
130
+ function pruneOldestMarkers(prefix: string, cap: number, deps: DispatchMarkerDeps): string[] {
131
+ const listed = deps
132
+ .runGit(['for-each-ref', '--format=%(refname)', '--sort=refname', `${prefix}/`])
133
+ .split('\n')
134
+ .map((l) => l.trim())
135
+ .filter(Boolean);
136
+
137
+ const excess = listed.length - cap;
138
+ if (excess <= 0) return [];
139
+
140
+ const doomed = listed.slice(0, excess);
141
+ for (const ref of doomed) {
142
+ deps.runGit(['update-ref', '-d', ref]);
143
+ }
144
+ return doomed;
145
+ }
146
+
147
+ /**
148
+ * Writes the boundary marker for `agentName` on `branch`.
149
+ *
150
+ * RETENTION KEY IS THE BRANCH, NOT THE WORKFLOW — a deliberate substitution,
151
+ * reported as such. The workflow id is only obtainable here through an async API
152
+ * call, which would make the marker ABSENT precisely when the system is degraded
153
+ * (API down) — the opposite of what a rollback coordinate is for. The branch is
154
+ * already in hand locally and needs no network. It is also the better key on the
155
+ * merits: `git reset --hard` is a branch-scoped operation, so grouping markers by
156
+ * branch matches the unit they would be consumed at.
157
+ *
158
+ * FAIL-OPEN IN EVERY DIMENSION: git missing, no commits yet, a ref-name that
159
+ * somehow still fails validation, a broken prune — all return a `failed` outcome.
160
+ * This function NEVER throws, so it can never block or delay a dispatch. It is
161
+ * fail-open but NOT silent: it returns the reason, and the caller surfaces it.
162
+ */
163
+ export function writeDispatchBoundaryMarker(
164
+ agentName: string,
165
+ branch: string,
166
+ deps: DispatchMarkerDeps
167
+ ): DispatchMarkerOutcome {
168
+ let ref = '';
169
+ try {
170
+ // An agent name that sanitizes to nothing yields a marker nobody can
171
+ // attribute — litter that consumes the retention cap rather than a usable
172
+ // coordinate. NOT a hypothetical guard: a `...-unknown` ref was written into
173
+ // the real repository during S445's negative-control run, because this hook's
174
+ // entry module executes on import (top-level `main().catch()`) and therefore
175
+ // ran with production deps and an empty stdin. The caller gates on
176
+ // isCommittingAgent, so this never fires in production — it is the backstop
177
+ // for the paths that bypass that gate.
178
+ if (sanitizeRefComponent(agentName) === EMPTY_COMPONENT_PLACEHOLDER) {
179
+ return {
180
+ status: 'failed',
181
+ reason: `refusing to write an unattributable marker (agent name "${agentName}")`,
182
+ };
183
+ }
184
+
185
+ const sha = deps.runGit(['rev-parse', 'HEAD']).trim();
186
+ if (!/^[0-9a-f]{40}$/.test(sha)) {
187
+ return { status: 'failed', reason: `HEAD did not resolve to a 40-hex sha (got "${sha}")` };
188
+ }
189
+
190
+ const stamp = formatMarkerStamp((deps.now ?? (() => new Date()))());
191
+ ref = buildMarkerRefName(branch, agentName, stamp);
192
+
193
+ deps.runGit(['update-ref', ref, sha]);
194
+
195
+ // The marker now exists. Pruning is best-effort bookkeeping ON TOP of a
196
+ // successful write — a prune failure must never downgrade the outcome, or a
197
+ // full namespace would start reporting failure for markers that were in fact
198
+ // written.
199
+ let pruned: string[] = [];
200
+ try {
201
+ pruned = pruneOldestMarkers(
202
+ `${DISPATCH_MARKER_REF_PREFIX}/${sanitizeRefComponent(branch)}`,
203
+ MAX_DISPATCH_MARKERS_PER_KEY,
204
+ deps
205
+ );
206
+ } catch {
207
+ pruned = [];
208
+ }
209
+
210
+ return { status: 'written', ref, sha, pruned };
211
+ } catch (err) {
212
+ return { status: 'failed', reason: `${ref ? `ref="${ref}" ` : ''}${String(err)}` };
213
+ }
214
+ }
@@ -3,7 +3,6 @@
3
3
  * Replaces post-phase-checkpoint.sh git logic.
4
4
  */
5
5
 
6
- import { execSync } from 'node:child_process';
7
6
  import { readFileSync, writeFileSync } from 'node:fs';
8
7
  import { join, dirname } from 'node:path';
9
8
  import { fileURLToPath } from 'node:url';
@@ -24,22 +23,33 @@ function getCompletedPhase(currentPhase: string): string | null {
24
23
  return idx > 0 ? (PHASE_ORDER[idx - 1] ?? null) : null;
25
24
  }
26
25
 
27
- function exec(cmd: string): string {
28
- return execSync(cmd, { cwd: PROJECT_ROOT, encoding: 'utf-8' }).trim();
29
- }
30
-
31
26
  /**
32
- * Create a git checkpoint if the phase changed and there are staged changes.
33
- * Returns the commit hash or null.
27
+ * Register a checkpoint (PostgreSQL side only) if the phase changed since the
28
+ * last checkpointed phase.
29
+ *
30
+ * TD-257 (S444): this function used to also stage and commit the working
31
+ * tree (`git add -A` + `git commit --no-verify`, unattended, on every phase
32
+ * transition). That was DISARMED after measurement — the decision was made
33
+ * by the user from data, not opinion: 212 such commits by subject over 7
34
+ * months, 145/212 (68%) pure machine-noise, 42/212 (20%) MIXED (real work
35
+ * buried alongside noise), one of which (`01c28adf`) deleted 1627 lines
36
+ * including `EvaluatePhaseGateUseCase.ts` and 3 suites under the message
37
+ * "Auto-checkpoint after tasks phase" — with zero restores in that window,
38
+ * and no production caller for `CheckpointManager.restore()` at all. The
39
+ * separate, deliberate `createCheckpoint` MCP tool (`CheckpointManager.ts`,
40
+ * `GitOperations.ts`) is UNAFFECTED by this change — it is named, tagged,
41
+ * and stays. Returns `true` when a checkpoint was registered for a newly
42
+ * completed phase, `false` otherwise (no completed phase, already
43
+ * checkpointed, or the registration call failed).
34
44
  */
35
45
  export async function createCheckpointIfNeeded(
36
46
  workflowId: string,
37
47
  currentPhase: string
38
- ): Promise<string | null> {
48
+ ): Promise<boolean> {
39
49
  const completedPhase = getCompletedPhase(currentPhase);
40
50
  if (!completedPhase) {
41
51
  log('CHECKPOINT', `No completed phase from: ${currentPhase}`);
42
- return null;
52
+ return false;
43
53
  }
44
54
 
45
55
  // Check if already checkpointed
@@ -51,41 +61,28 @@ export async function createCheckpointIfNeeded(
51
61
  }
52
62
  if (lastPhase === completedPhase) {
53
63
  log('CHECKPOINT', `Already checkpointed: ${completedPhase}`);
54
- return null;
64
+ return false;
55
65
  }
56
66
 
57
67
  try {
58
- // Stage all changes
59
- exec('git add -A');
60
-
61
- // Check if there are staged changes
62
- try {
63
- exec('git diff --cached --quiet');
64
- // No changes
65
- log('CHECKPOINT', `No changes for phase: ${completedPhase}`);
66
- writeFileSync(LAST_PHASE_FILE, completedPhase);
67
- return null;
68
- } catch {
69
- // Has changes — good, proceed to commit
70
- }
71
-
72
- const commitMsg = `[orchestrator] ${completedPhase}: Auto-checkpoint after ${completedPhase} phase`;
73
- exec(`git commit -m "${commitMsg}" --no-verify`);
74
- const commitHash = exec('git rev-parse HEAD');
75
-
76
- log('CHECKPOINT', `Created: phase=${completedPhase} commit=${commitHash}`);
68
+ log('CHECKPOINT', `Registering checkpoint for phase: ${completedPhase}`);
77
69
 
78
- // Register via API
79
- await registerCheckpoint(
80
- workflowId,
81
- `Auto-checkpoint after ${completedPhase} phase`,
82
- commitHash
83
- );
70
+ // TD-257: no commit is made anymore, so there is no real hash to report.
71
+ // registerCheckpoint's `commitHash` parameter is still required by its
72
+ // signature (out of this task's scope to change — see implementation
73
+ // report) — '' is the honest value ("no commit exists"), not an invented
74
+ // placeholder: it mirrors how the separate createCheckpoint MCP tool path
75
+ // already reports "no commit" as `gitCommit: undefined`
76
+ // (CreateCheckpointHandler.ts) when there is nothing to point to, and the
77
+ // field is unused server-side regardless — CreateCheckpointBodySchema
78
+ // (checkpoints.schemas.ts) has no commitHash field at all, and the route
79
+ // handler (checkpoints.routes.ts) never reads it off the request body.
80
+ await registerCheckpoint(workflowId, `Auto-checkpoint after ${completedPhase} phase`, '');
84
81
 
85
82
  writeFileSync(LAST_PHASE_FILE, completedPhase);
86
- return commitHash;
83
+ return true;
87
84
  } catch (err) {
88
85
  log('CHECKPOINT', `Error: ${err}`);
89
- return null;
86
+ return false;
90
87
  }
91
88
  }
@@ -4,11 +4,22 @@
4
4
  * Provides types and helper to write observations.yaml files.
5
5
  * Used by subagent-stop.ts to record what each sub-agent observed.
6
6
  * All functions are fail-open: errors logged to stderr, never thrown.
7
+ *
8
+ * TD-214/TD-239 TASK-003 (RF-004): `agent` is normalized via
9
+ * `normalizeAgentName` BEFORE it is used to build `agentDir`. Under plugin
10
+ * scope the harness namespaces dispatch names (`orchestrator:reviewer`); in
11
+ * project scope the same agent arrives bare (`reviewer`). Without this,
12
+ * the two scopes silently fork into two different observations.yaml
13
+ * directories for the SAME agent. `data.agent` (the field written INSIDE the
14
+ * YAML) intentionally keeps the raw, un-normalized input — only the
15
+ * directory-path use is normalized — so the file still records exactly what
16
+ * the harness sent, for audit purposes.
7
17
  */
8
18
 
9
19
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
10
20
  import { join } from 'node:path';
11
21
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
22
+ import { normalizeAgentName } from './api-client.js';
12
23
 
13
24
  // ─── Types ───────────────────────────────────────────────
14
25
 
@@ -47,7 +58,9 @@ export function appendObservation(
47
58
  entry: Omit<ObservationEntry, 'promoted'>
48
59
  ): void {
49
60
  try {
50
- const agentDir = join(memoryDir, agent);
61
+ // TD-214/TD-239 TASK-003: normalize BEFORE building the directory path —
62
+ // see module doc for why (plugin-scope vs project-scope namespacing).
63
+ const agentDir = join(memoryDir, normalizeAgentName(agent));
51
64
  const filePath = join(agentDir, 'observations.yaml');
52
65
 
53
66
  // Ensure directory exists
@@ -6,28 +6,80 @@ import {
6
6
  readFileSync as readFileSync2,
7
7
  unlinkSync as unlinkSync2,
8
8
  renameSync,
9
- existsSync,
9
+ existsSync as existsSync2,
10
10
  mkdirSync as mkdirSync2,
11
11
  writeFileSync as writeFileSync2
12
12
  } from "node:fs";
13
- import { join as join2 } from "node:path";
13
+ import { join as join3 } from "node:path";
14
14
  import { randomUUID } from "node:crypto";
15
15
 
16
16
  // hooks/lib/api-client.ts
17
17
  import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
18
- import { join, dirname } from "node:path";
19
- import { fileURLToPath } from "node:url";
20
- var __filename = fileURLToPath(import.meta.url);
21
- var __dirname = dirname(__filename);
22
- var PROJECT_ROOT = join(__dirname, "..", "..", "..");
23
- var STATE_DIR = join(PROJECT_ROOT, ".orchestrator", ".state");
24
- var JWT_CACHE = join(STATE_DIR, "jwt-token");
25
- var DEBUG_LOG = join(STATE_DIR, "hook-debug.log");
18
+ import { join as join2 } from "node:path";
19
+ import { tmpdir } from "node:os";
20
+ import { createHash } from "node:crypto";
21
+
22
+ // ../project-root-resolver/dist/index.js
23
+ import { existsSync } from "node:fs";
24
+ import { dirname, join } from "node:path";
25
+ function hasProjectRootMarker(candidate, sentinel) {
26
+ return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
27
+ }
28
+ function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
29
+ const candidates = [];
30
+ let currentDir = cwd;
31
+ for (; ; ) {
32
+ if (existsSync(join(currentDir, sentinel))) {
33
+ candidates.push(currentDir);
34
+ }
35
+ const parentDir = dirname(currentDir);
36
+ if (parentDir === currentDir) {
37
+ break;
38
+ }
39
+ currentDir = parentDir;
40
+ }
41
+ const [innermost] = candidates;
42
+ if (innermost === void 0) {
43
+ return null;
44
+ }
45
+ for (const candidate of candidates) {
46
+ if (hasProjectRootMarker(candidate, sentinel)) {
47
+ return candidate;
48
+ }
49
+ }
50
+ return innermost;
51
+ }
52
+
53
+ // hooks/lib/api-client.ts
54
+ function resolveFallbackStateDir(cwd) {
55
+ const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
56
+ return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
57
+ }
58
+ var PROJECT_ROOT = (() => {
59
+ const resolved = resolveHookProjectRoot(process.cwd());
60
+ if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
61
+ return resolved;
62
+ }
63
+ return null;
64
+ })();
65
+ var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
66
+ var STATE_DIR = STATE_DIR_OVERRIDE !== void 0 && STATE_DIR_OVERRIDE !== "" ? STATE_DIR_OVERRIDE : PROJECT_ROOT !== null ? join2(PROJECT_ROOT, ".orchestrator", ".state") : resolveFallbackStateDir(process.cwd());
67
+ var JWT_CACHE = join2(STATE_DIR, "jwt-token");
68
+ var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
26
69
  mkdirSync(STATE_DIR, { recursive: true });
70
+ if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
71
+ log(
72
+ "API-CLIENT",
73
+ `PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
74
+ );
75
+ }
27
76
  var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
28
77
  var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
29
78
  var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
30
79
  var PROJECT_ID = process.env.ORCHESTRATOR_PROJECT_ID || "";
80
+ function resolveHookProjectRoot(cwd) {
81
+ return resolveProjectRoot(cwd);
82
+ }
31
83
  function log(prefix, msg) {
32
84
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
33
85
  try {
@@ -37,11 +89,11 @@ function log(prefix, msg) {
37
89
  } catch {
38
90
  }
39
91
  }
40
- var LAST_COMPLETED_FILE = join(STATE_DIR, "last-completed-workflow.json");
92
+ var LAST_COMPLETED_FILE = join2(STATE_DIR, "last-completed-workflow.json");
41
93
  var LAST_COMPLETED_TTL_MS = 60 * 60 * 1e3;
42
- var INVOCATIONS_DIR = join(STATE_DIR, "invocations");
94
+ var INVOCATIONS_DIR = join2(STATE_DIR, "invocations");
43
95
  var INVOCATION_STATE_TTL_MS = 4 * 60 * 60 * 1e3;
44
- var DISPATCH_INTENT_DIR = join(STATE_DIR, "dispatch-intent");
96
+ var DISPATCH_INTENT_DIR = join2(STATE_DIR, "dispatch-intent");
45
97
  function outputContext(hookEvent, context) {
46
98
  console.log(
47
99
  JSON.stringify({
@@ -82,7 +134,7 @@ function consumeInbox(inboxPath, quarantinePath, paneRole) {
82
134
  const consumed = [];
83
135
  const errors = [];
84
136
  for (const file of files) {
85
- const filePath = join2(inboxPath, file);
137
+ const filePath = join3(inboxPath, file);
86
138
  try {
87
139
  const raw = readFileSync2(filePath, "utf-8");
88
140
  const parsed = JSON.parse(raw);
@@ -101,7 +153,7 @@ function consumeInbox(inboxPath, quarantinePath, paneRole) {
101
153
  log(HOOK, `Quarantine: ${file} \u2014 ${errMsg}`);
102
154
  try {
103
155
  mkdirSync2(quarantinePath, { recursive: true });
104
- renameSync(filePath, join2(quarantinePath, file));
156
+ renameSync(filePath, join3(quarantinePath, file));
105
157
  } catch {
106
158
  }
107
159
  }
@@ -211,7 +263,7 @@ function relaySidecarMessages(consumed, sprintId, paneRole, deps = { readdirSync
211
263
  };
212
264
  const sidecarAddr = `${sidecar.role}-${String(sidecar.index)}`;
213
265
  const targetInbox = `/tmp/${sprintId}/mailbox/${sidecarAddr}/inbox`;
214
- const targetPath = join2(targetInbox, `${envelope.id}.json`);
266
+ const targetPath = join3(targetInbox, `${envelope.id}.json`);
215
267
  deps.writeFileSync(targetPath, JSON.stringify(envelope), "utf-8");
216
268
  log(HOOK, `Relayed task_complete to ${sidecarAddr}: ${envelope.id}`);
217
269
  } catch (err) {
@@ -229,7 +281,7 @@ async function main() {
229
281
  }
230
282
  const inboxPath = `/tmp/${sprintId}/mailbox/${paneRole}/inbox`;
231
283
  const quarantinePath = `/tmp/${sprintId}/mailbox/${paneRole}/quarantine`;
232
- if (!existsSync(inboxPath)) {
284
+ if (!existsSync2(inboxPath)) {
233
285
  return;
234
286
  }
235
287
  let hookEventName;
@@ -1,38 +1,68 @@
1
1
  import { createRequire as __createRequireShim } from 'node:module'; const require = __createRequireShim(import.meta.url);
2
2
 
3
3
  // hooks/playwright-output-guard.ts
4
- import { dirname as dirname3, isAbsolute, normalize, resolve } from "node:path";
4
+ import { dirname as dirname2, isAbsolute, normalize, resolve } from "node:path";
5
5
 
6
6
  // hooks/lib/api-client.ts
7
7
  import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
8
- import { join as join2, dirname as dirname2 } from "node:path";
9
- import { fileURLToPath } from "node:url";
8
+ import { join as join2 } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+ import { createHash } from "node:crypto";
10
11
 
11
12
  // ../project-root-resolver/dist/index.js
12
13
  import { existsSync } from "node:fs";
13
14
  import { dirname, join } from "node:path";
15
+ function hasProjectRootMarker(candidate, sentinel) {
16
+ return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
17
+ }
14
18
  function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
19
+ const candidates = [];
15
20
  let currentDir = cwd;
16
21
  for (; ; ) {
17
22
  if (existsSync(join(currentDir, sentinel))) {
18
- return currentDir;
23
+ candidates.push(currentDir);
19
24
  }
20
25
  const parentDir = dirname(currentDir);
21
26
  if (parentDir === currentDir) {
22
- return null;
27
+ break;
23
28
  }
24
29
  currentDir = parentDir;
25
30
  }
31
+ const [innermost] = candidates;
32
+ if (innermost === void 0) {
33
+ return null;
34
+ }
35
+ for (const candidate of candidates) {
36
+ if (hasProjectRootMarker(candidate, sentinel)) {
37
+ return candidate;
38
+ }
39
+ }
40
+ return innermost;
26
41
  }
27
42
 
28
43
  // hooks/lib/api-client.ts
29
- var __filename = fileURLToPath(import.meta.url);
30
- var __dirname = dirname2(__filename);
31
- var PROJECT_ROOT = join2(__dirname, "..", "..", "..");
32
- var STATE_DIR = join2(PROJECT_ROOT, ".orchestrator", ".state");
44
+ function resolveFallbackStateDir(cwd) {
45
+ const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
46
+ return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
47
+ }
48
+ var PROJECT_ROOT = (() => {
49
+ const resolved = resolveHookProjectRoot(process.cwd());
50
+ if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
51
+ return resolved;
52
+ }
53
+ return null;
54
+ })();
55
+ var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
56
+ var STATE_DIR = STATE_DIR_OVERRIDE !== void 0 && STATE_DIR_OVERRIDE !== "" ? STATE_DIR_OVERRIDE : PROJECT_ROOT !== null ? join2(PROJECT_ROOT, ".orchestrator", ".state") : resolveFallbackStateDir(process.cwd());
33
57
  var JWT_CACHE = join2(STATE_DIR, "jwt-token");
34
58
  var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
35
59
  mkdirSync(STATE_DIR, { recursive: true });
60
+ if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
61
+ log(
62
+ "API-CLIENT",
63
+ `PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
64
+ );
65
+ }
36
66
  var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
37
67
  var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
38
68
  var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
@@ -95,7 +125,7 @@ var GUIDANCE = 'Playwright Output Guard: screenshots n\xE3o podem cair na raiz d
95
125
  function evaluateFilename(filename) {
96
126
  if (!filename) return { decision: "allow" };
97
127
  const resolved = isAbsolute(filename) ? normalize(filename) : resolve(PROJECT_ROOT2, filename);
98
- const dir = dirname3(resolved);
128
+ const dir = dirname2(resolved);
99
129
  if (dir === PROJECT_ROOT2) {
100
130
  return { decision: "deny", reason: GUIDANCE };
101
131
  }
@@ -2,34 +2,64 @@ import { createRequire as __createRequireShim } from 'node:module'; const requir
2
2
 
3
3
  // hooks/lib/api-client.ts
4
4
  import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
5
- import { join as join2, dirname as dirname2 } from "node:path";
6
- import { fileURLToPath } from "node:url";
5
+ import { join as join2 } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { createHash } from "node:crypto";
7
8
 
8
9
  // ../project-root-resolver/dist/index.js
9
10
  import { existsSync } from "node:fs";
10
11
  import { dirname, join } from "node:path";
12
+ function hasProjectRootMarker(candidate, sentinel) {
13
+ return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
14
+ }
11
15
  function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
16
+ const candidates = [];
12
17
  let currentDir = cwd;
13
18
  for (; ; ) {
14
19
  if (existsSync(join(currentDir, sentinel))) {
15
- return currentDir;
20
+ candidates.push(currentDir);
16
21
  }
17
22
  const parentDir = dirname(currentDir);
18
23
  if (parentDir === currentDir) {
19
- return null;
24
+ break;
20
25
  }
21
26
  currentDir = parentDir;
22
27
  }
28
+ const [innermost] = candidates;
29
+ if (innermost === void 0) {
30
+ return null;
31
+ }
32
+ for (const candidate of candidates) {
33
+ if (hasProjectRootMarker(candidate, sentinel)) {
34
+ return candidate;
35
+ }
36
+ }
37
+ return innermost;
23
38
  }
24
39
 
25
40
  // hooks/lib/api-client.ts
26
- var __filename = fileURLToPath(import.meta.url);
27
- var __dirname = dirname2(__filename);
28
- var PROJECT_ROOT = join2(__dirname, "..", "..", "..");
29
- var STATE_DIR = join2(PROJECT_ROOT, ".orchestrator", ".state");
41
+ function resolveFallbackStateDir(cwd) {
42
+ const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
43
+ return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
44
+ }
45
+ var PROJECT_ROOT = (() => {
46
+ const resolved = resolveHookProjectRoot(process.cwd());
47
+ if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
48
+ return resolved;
49
+ }
50
+ return null;
51
+ })();
52
+ var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
53
+ var STATE_DIR = STATE_DIR_OVERRIDE !== void 0 && STATE_DIR_OVERRIDE !== "" ? STATE_DIR_OVERRIDE : PROJECT_ROOT !== null ? join2(PROJECT_ROOT, ".orchestrator", ".state") : resolveFallbackStateDir(process.cwd());
30
54
  var JWT_CACHE = join2(STATE_DIR, "jwt-token");
31
55
  var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
32
56
  mkdirSync(STATE_DIR, { recursive: true });
57
+ if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
58
+ log(
59
+ "API-CLIENT",
60
+ `PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
61
+ );
62
+ }
33
63
  var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
34
64
  var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
35
65
  var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";