@orchestrator-claude/definitions 3.37.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} +8 -3
  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 +10 -5
  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
@@ -3925,7 +3925,7 @@ var init_esm5 = __esm({
3925
3925
 
3926
3926
  // ../../node_modules/path-scurry/dist/esm/index.js
3927
3927
  import { posix, win32 } from "node:path";
3928
- import { fileURLToPath as fileURLToPath2 } from "node:url";
3928
+ import { fileURLToPath } from "node:url";
3929
3929
  import { lstatSync, readdir as readdirCB, readdirSync as readdirSync2, readlinkSync, realpathSync as rps } from "fs";
3930
3930
  import * as actualFS from "node:fs";
3931
3931
  import { lstat, readdir, readlink, realpath } from "node:fs/promises";
@@ -5057,7 +5057,7 @@ var init_esm6 = __esm({
5057
5057
  constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs = defaultFS } = {}) {
5058
5058
  this.#fs = fsFromOption(fs);
5059
5059
  if (cwd instanceof URL || cwd.startsWith("file://")) {
5060
- cwd = fileURLToPath2(cwd);
5060
+ cwd = fileURLToPath(cwd);
5061
5061
  }
5062
5062
  const cwdPath = pathImpl.resolve(cwd);
5063
5063
  this.roots = /* @__PURE__ */ Object.create(null);
@@ -6491,7 +6491,7 @@ var init_walker = __esm({
6491
6491
  });
6492
6492
 
6493
6493
  // ../../node_modules/glob/dist/esm/glob.js
6494
- import { fileURLToPath as fileURLToPath3 } from "node:url";
6494
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6495
6495
  var defaultPlatform3, Glob;
6496
6496
  var init_glob = __esm({
6497
6497
  "../../node_modules/glob/dist/esm/glob.js"() {
@@ -6559,7 +6559,7 @@ var init_glob = __esm({
6559
6559
  if (!opts.cwd) {
6560
6560
  this.cwd = "";
6561
6561
  } else if (opts.cwd instanceof URL || opts.cwd.startsWith("file://")) {
6562
- opts.cwd = fileURLToPath3(opts.cwd);
6562
+ opts.cwd = fileURLToPath2(opts.cwd);
6563
6563
  }
6564
6564
  this.cwd = opts.cwd || "";
6565
6565
  this.root = opts.root;
@@ -6799,34 +6799,64 @@ var init_esm7 = __esm({
6799
6799
 
6800
6800
  // hooks/lib/api-client.ts
6801
6801
  import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
6802
- import { join as join2, dirname as dirname2 } from "node:path";
6803
- import { fileURLToPath } from "node:url";
6802
+ import { join as join2 } from "node:path";
6803
+ import { tmpdir } from "node:os";
6804
+ import { createHash } from "node:crypto";
6804
6805
 
6805
6806
  // ../project-root-resolver/dist/index.js
6806
6807
  import { existsSync } from "node:fs";
6807
6808
  import { dirname, join } from "node:path";
6809
+ function hasProjectRootMarker(candidate, sentinel) {
6810
+ return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
6811
+ }
6808
6812
  function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
6813
+ const candidates = [];
6809
6814
  let currentDir = cwd;
6810
6815
  for (; ; ) {
6811
6816
  if (existsSync(join(currentDir, sentinel))) {
6812
- return currentDir;
6817
+ candidates.push(currentDir);
6813
6818
  }
6814
6819
  const parentDir = dirname(currentDir);
6815
6820
  if (parentDir === currentDir) {
6816
- return null;
6821
+ break;
6817
6822
  }
6818
6823
  currentDir = parentDir;
6819
6824
  }
6825
+ const [innermost] = candidates;
6826
+ if (innermost === void 0) {
6827
+ return null;
6828
+ }
6829
+ for (const candidate of candidates) {
6830
+ if (hasProjectRootMarker(candidate, sentinel)) {
6831
+ return candidate;
6832
+ }
6833
+ }
6834
+ return innermost;
6820
6835
  }
6821
6836
 
6822
6837
  // hooks/lib/api-client.ts
6823
- var __filename = fileURLToPath(import.meta.url);
6824
- var __dirname = dirname2(__filename);
6825
- var PROJECT_ROOT = join2(__dirname, "..", "..", "..");
6826
- var STATE_DIR = join2(PROJECT_ROOT, ".orchestrator", ".state");
6838
+ function resolveFallbackStateDir(cwd) {
6839
+ const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
6840
+ return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
6841
+ }
6842
+ var PROJECT_ROOT = (() => {
6843
+ const resolved = resolveHookProjectRoot(process.cwd());
6844
+ if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
6845
+ return resolved;
6846
+ }
6847
+ return null;
6848
+ })();
6849
+ var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
6850
+ 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());
6827
6851
  var JWT_CACHE = join2(STATE_DIR, "jwt-token");
6828
6852
  var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
6829
6853
  mkdirSync(STATE_DIR, { recursive: true });
6854
+ if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
6855
+ log(
6856
+ "API-CLIENT",
6857
+ `PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
6858
+ );
6859
+ }
6830
6860
  var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
6831
6861
  var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
6832
6862
  var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
@@ -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";
package/hooks/hooks.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "SessionStart": [
4
4
  {
5
- "matcher": "startup|resume",
5
+ "matcher": "startup|resume|clear|compact|fork",
6
6
  "hooks": [
7
7
  { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"", "timeout": 10000 },
8
8
  { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/sprint-registry.mjs\"", "timeout": 5000 }
@@ -42,13 +42,13 @@
42
42
  ],
43
43
  "SubagentStart": [
44
44
  {
45
- "matcher": "(orchestrator:)?(implementer|specifier|planner|task-generator|reviewer|researcher)",
45
+ "matcher": "(orchestrator:)?(adversarial-reviewer|api-extractor|azure-devops-engineer|business-rule-miner|cicd-platform-engineer|ciclo-engineer|code-archaeologist|debug-sidecar|doc-sidecar|docs-guardian|frontend-engineer|harness-architect|implementer|legacy-discoverer|legacy-synthesizer|methodology-researcher|operational-artifact-builder|orchestrator|planner|presentation-engineer|python-data-engineer|researcher|reviewer|schema-extractor|software-architect|specifier|task-generator|test-sidecar)",
46
46
  "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-start.mjs\"", "timeout": 10000 }]
47
47
  }
48
48
  ],
49
49
  "SubagentStop": [
50
50
  {
51
- "matcher": "(orchestrator:)?(implementer|specifier|planner|task-generator|reviewer|researcher)",
51
+ "matcher": "(orchestrator:)?(adversarial-reviewer|api-extractor|azure-devops-engineer|business-rule-miner|cicd-platform-engineer|ciclo-engineer|code-archaeologist|debug-sidecar|doc-sidecar|docs-guardian|frontend-engineer|harness-architect|implementer|legacy-discoverer|legacy-synthesizer|methodology-researcher|operational-artifact-builder|orchestrator|planner|presentation-engineer|python-data-engineer|researcher|reviewer|schema-extractor|software-architect|specifier|task-generator|test-sidecar)",
52
52
  "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop.mjs\"", "timeout": 30000 }]
53
53
  }
54
54
  ],
@@ -67,12 +67,6 @@
67
67
  "matcher": "mcp__(plugin_orchestrator_)?orchestrator-tools__advancePhase",
68
68
  "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/post-implement.mjs\"", "timeout": 15000 }]
69
69
  }
70
- ],
71
- "PostCompact": [
72
- {
73
- "matcher": "",
74
- "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/post-compact.mjs\"", "timeout": 5000 }]
75
- }
76
70
  ]
77
71
  }
78
72
  }
@@ -4,21 +4,119 @@
4
4
  */
5
5
 
6
6
  import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from 'node:fs';
7
- import { join, dirname } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { resolveProjectRoot } from '@orchestrator-claude/project-root-resolver';
10
-
11
- // Resolve paths relative to this file
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
14
- const PROJECT_ROOT = join(__dirname, '..', '..', '..');
15
- const STATE_DIR = join(PROJECT_ROOT, '.orchestrator', '.state');
7
+ import { join } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import { createHash } from 'node:crypto';
10
+ import {
11
+ resolveProjectRoot,
12
+ hasProjectRootMarker,
13
+ } from '@orchestrator-claude/project-root-resolver';
14
+
15
+ // S467 H-1 (TASK-004): `__filename`/`__dirname` (this file's own on-disk
16
+ // location) used to anchor PROJECT_ROOT below — removed together with that
17
+ // derivation. See the RF-001/RF-002/RF-003 comment block ahead of
18
+ // PROJECT_ROOT for why anchoring on this file's own location was the bug.
19
+
20
+ /**
21
+ * RF-003 (S467, H-1): pure, zero-throw fallback used when the calling
22
+ * session's own cwd has no resolvable, marker-verified `.orchestrator`
23
+ * project root (see PROJECT_ROOT below) — e.g. an arbitrary directory, or a
24
+ * bare/incidental sentinel that fails the marker check (RF-002). Isolated
25
+ * per-cwd via a stable hash so two distinct unresolved cwds never collide
26
+ * on the same fallback directory (verified:
27
+ * api-client-project-root-derivation.test.ts, RF-003 anti-collision case),
28
+ * and deliberately OUTSIDE this repo's own tree (os.tmpdir()) so it can
29
+ * never be mistaken for, or leak into, this repo's own `.orchestrator/.state`.
30
+ */
31
+ export function resolveFallbackStateDir(cwd: string): string {
32
+ const cwdHash = createHash('sha256').update(cwd).digest('hex').slice(0, 16);
33
+ return join(tmpdir(), 'orchestrator-hook-fallback', cwdHash);
34
+ }
35
+
36
+ // TD-224 Achado 0 (S426) superseded by S467 H-1 (RF-001/RF-002/RF-003): this
37
+ // used to derive PROJECT_ROOT via `resolveProjectRoot(__dirname)` —
38
+ // `__dirname` is the location of THIS FILE ON DISK, not the calling
39
+ // session's own working directory. Because the `orchestrator-local`
40
+ // marketplace resolves by `directory` (marketplace.json `source:
41
+ // "./packages/definitions"`), every hook process on this machine, for every
42
+ // session of every consumer project, has `__dirname` inside THIS repo's own
43
+ // checkout — so the walk-up always found and won on THIS repo's
44
+ // `.orchestrator` sentinel, regardless of which project's session actually
45
+ // invoked the hook. Cross-project state leakage (H-1).
46
+ //
47
+ // RF-001: anchor on `process.cwd()` — the session's own cwd — via the
48
+ // already-exported `resolveHookProjectRoot` wrapper below, instead of
49
+ // `__dirname`.
50
+ //
51
+ // RF-002: a bare `.orchestrator` sentinel with none of
52
+ // `.git`/`package.json`/`.orchestrator/.root` (e.g. a real user home
53
+ // directory that happens to carry a bare `.orchestrator` dir — measured
54
+ // S466/S467 against this very machine's `/home/cmac`) must NOT resolve to
55
+ // itself. `resolveProjectRoot`'s own backward-compatible fallback
56
+ // (innermost-wins when no candidate satisfies the marker check, ADR-P3)
57
+ // would otherwise happily return it — reject that case explicitly via the
58
+ // now-exported `hasProjectRootMarker` (ADR-P3, TASK-003) and fall through
59
+ // to RF-003.
60
+ //
61
+ // RF-003: never `throw` at module load. The prior comment here justified
62
+ // throwing by citing `.orchestrator/.state/.gitkeep` (tracked at this
63
+ // repo's own root) as a guarantee the sentinel is "always present" — that
64
+ // guarantee held only because the OLD code anchored on `__dirname` (always
65
+ // inside this repo). It stops applying once the anchor is `process.cwd()`
66
+ // of an arbitrary session: a hook process serving ANY unregistered cwd must
67
+ // still load and degrade gracefully into an isolated, per-cwd fallback
68
+ // state directory (`resolveFallbackStateDir` above), not crash every hook
69
+ // invocation for that session (RNF-001, availability).
70
+ //
71
+ // ORCHESTRATOR_STATE_DIR (TD-224 Achado 1, RACE-001) is resolved separately
72
+ // below and continues to win unconditionally over ALL of this — checked
73
+ // before any consumer of PROJECT_ROOT/STATE_DIR ever runs.
74
+ const PROJECT_ROOT = ((): string | null => {
75
+ const resolved = resolveHookProjectRoot(process.cwd());
76
+ if (resolved !== null && hasProjectRootMarker(resolved, '.orchestrator')) {
77
+ return resolved;
78
+ }
79
+ return null;
80
+ })();
81
+ // TD-224 Achado 1 (S426, RACE-001): ORCHESTRATOR_STATE_DIR, when set, wins
82
+ // outright over the .orchestrator-sentinel-derived PROJECT_ROOT — every
83
+ // state-file path below (JWT_CACHE, DEBUG_LOG, invocation/dispatch-intent
84
+ // state) resolves inside the override directory instead. Motivation:
85
+ // tests/unit/seam-conformance's execution extractor drives every exported
86
+ // function of this module for real (fetch mocked, everything else live) to
87
+ // build a route census; without this seam, the ONLY way to keep it from
88
+ // touching the REAL dev-session jwt-token cache was a backup/seed/restore
89
+ // dance around the real path — unsafe both under concurrent test-file
90
+ // execution (two vitest worker threads) and under process interruption (a
91
+ // kill between "seed" and "restore" corrupts the real cache with zero
92
+ // concurrency required). Pointing this override at an isolated temp
93
+ // directory removes the real path from the equation entirely: no window,
94
+ // concurrent or interrupted, in which it is ever opened. Unset in every
95
+ // production invocation — default derivation is unchanged.
96
+ const STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
97
+ const STATE_DIR =
98
+ STATE_DIR_OVERRIDE !== undefined && STATE_DIR_OVERRIDE !== ''
99
+ ? STATE_DIR_OVERRIDE
100
+ : PROJECT_ROOT !== null
101
+ ? join(PROJECT_ROOT, '.orchestrator', '.state')
102
+ : resolveFallbackStateDir(process.cwd());
16
103
  const JWT_CACHE = join(STATE_DIR, 'jwt-token');
17
104
  const DEBUG_LOG = join(STATE_DIR, 'hook-debug.log');
18
105
 
19
106
  // Ensure state dir exists
20
107
  mkdirSync(STATE_DIR, { recursive: true });
21
108
 
109
+ // RF-003: log the fallback degradation, once, at module load — but only
110
+ // when it actually happened (override absent AND no marker-verified
111
+ // project root resolved for this cwd).
112
+ if ((STATE_DIR_OVERRIDE === undefined || STATE_DIR_OVERRIDE === '') && PROJECT_ROOT === null) {
113
+ log(
114
+ 'API-CLIENT',
115
+ `PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} — ` +
116
+ `falling back to isolated state dir ${STATE_DIR} (RF-003)`
117
+ );
118
+ }
119
+
22
120
  // Config from env
23
121
  const API_URL = process.env.ORCHESTRATOR_API_URL || 'http://localhost:3001';
24
122
  // RFC-040 ADR-002 (S358, TASK-RBAC-02): hooks authenticate as the seeded
@@ -153,6 +251,27 @@ export function setEffectiveProjectId(projectId: string): void {
153
251
  PROJECT_ID = projectId;
154
252
  }
155
253
 
254
+ // --- Agent Identity Normalization (TD-214/TD-239 TASK-001) ---
255
+
256
+ /**
257
+ * Strip the plugin namespace from a dispatch name.
258
+ *
259
+ * Under plugin scope the harness namespaces every agent (`orchestrator:implementer`
260
+ * — measured in the S398 sandbox, RFC-040 §18.14); in project scope the same agent
261
+ * arrives bare (`implementer`). A guard that compares against the bare name only
262
+ * answers `false` for the namespaced form — and since hooks fail open, it disables
263
+ * itself in silence. Both forms MUST resolve to the same decision.
264
+ *
265
+ * Moved here from `pre-agent-dispatch.ts` (TD-214/TD-239 TASK-001) so every hook
266
+ * that needs identity normalization shares ONE implementation instead of
267
+ * hand-rolling its own `lastIndexOf(':')` substring logic — see TASK-002's
268
+ * consolidator test for the full list of consumers this is meant to unify.
269
+ */
270
+ export function normalizeAgentName(subagentType: string): string {
271
+ const i = subagentType.lastIndexOf(':');
272
+ return i === -1 ? subagentType : subagentType.slice(i + 1);
273
+ }
274
+
156
275
  // --- Types ---
157
276
 
158
277
  export interface WorkflowStatus {