@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.
- package/agents/adversarial-reviewer.md +6 -1
- package/agents/api-extractor.md +6 -0
- package/agents/azure-devops-engineer.md +1 -2
- package/agents/business-rule-miner.md +6 -0
- package/agents/cicd-platform-engineer.md +1 -2
- package/agents/ciclo-engineer.md +15 -2
- package/agents/code-archaeologist.md +6 -0
- package/agents/debug-sidecar.md +6 -0
- package/agents/doc-sidecar.md +6 -0
- package/agents/docs-guardian.md +9 -3
- package/agents/frontend-engineer.md +1 -2
- package/agents/{ai-architect.md → harness-architect.md} +26 -5
- package/agents/implementer.md +6 -1
- package/agents/legacy-discoverer.md +6 -0
- package/agents/legacy-synthesizer.md +6 -0
- package/agents/methodology-researcher.md +6 -1
- package/agents/operational-artifact-builder.md +6 -1
- package/agents/orchestrator.md +27 -87
- package/agents/planner.md +6 -1
- package/agents/presentation-engineer.md +5 -4
- package/agents/python-data-engineer.md +0 -1
- package/agents/researcher.md +6 -0
- package/agents/reviewer.md +6 -1
- package/agents/schema-extractor.md +6 -0
- package/agents/software-architect.md +30 -7
- package/agents/specifier.md +6 -1
- package/agents/task-generator.md +6 -1
- package/agents/test-sidecar.md +6 -0
- package/dist/scripts/generate-hooks-matcher.d.ts +106 -0
- package/dist/scripts/generate-hooks-matcher.d.ts.map +1 -0
- package/dist/scripts/generate-hooks-matcher.js +153 -0
- package/dist/scripts/generate-hooks-matcher.js.map +1 -0
- package/dist/scripts/generate-manifest.d.ts +22 -0
- package/dist/scripts/generate-manifest.d.ts.map +1 -1
- package/dist/scripts/generate-manifest.js +67 -2
- package/dist/scripts/generate-manifest.js.map +1 -1
- package/hooks/agent-git-guard.mjs +63 -11
- package/hooks/dangling-guard.mjs +42 -12
- package/hooks/gate-guardian.mjs +38 -8
- package/hooks/hooks.json +3 -9
- package/hooks/lib/api-client.ts +128 -9
- package/hooks/lib/capability-scanner.ts +391 -55
- package/hooks/lib/dispatch-marker.ts +214 -0
- package/hooks/lib/git-checkpoint.ts +34 -37
- package/hooks/lib/observations-helper.ts +14 -1
- package/hooks/mailbox-listener.mjs +69 -17
- package/hooks/playwright-output-guard.mjs +40 -10
- package/hooks/post-implement.mjs +38 -8
- package/hooks/pre-agent-dispatch.mjs +137 -16
- package/hooks/pre-agent-dispatch.ts +82 -15
- package/hooks/session-start.mjs +274 -103
- package/hooks/session-start.ts +250 -69
- package/hooks/subagent-start.mjs +7665 -22
- package/hooks/subagent-start.ts +367 -16
- package/hooks/subagent-stop.mjs +102 -62
- package/hooks/subagent-stop.ts +124 -28
- package/hooks/user-prompt.mjs +43 -13
- package/hooks/verification-before-completion.mjs +63 -11
- package/hooks/workflow-guard.mjs +45 -11
- package/hooks-settings-template.json +1 -7
- package/manifest.json +62 -62
- package/package.json +4 -2
- package/skills/checkpoint-protocol/SKILL.md +19 -4
- package/skills/ciclo-lane-dispatch/SKILL.md +24 -0
- package/skills/fase-1b-projecao/SKILL.md +146 -0
- package/skills/orchestrator/SKILL.md +11 -3
- package/skills/workflow-coordination/SKILL.md +3 -3
- package/hooks/post-compact.mjs +0 -7931
- 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
|
-
*
|
|
33
|
-
*
|
|
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<
|
|
48
|
+
): Promise<boolean> {
|
|
39
49
|
const completedPhase = getCompletedPhase(currentPhase);
|
|
40
50
|
if (!completedPhase) {
|
|
41
51
|
log('CHECKPOINT', `No completed phase from: ${currentPhase}`);
|
|
42
|
-
return
|
|
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
|
|
64
|
+
return false;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
try {
|
|
58
|
-
|
|
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
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
83
|
+
return true;
|
|
87
84
|
} catch (err) {
|
|
88
85
|
log('CHECKPOINT', `Error: ${err}`);
|
|
89
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 =
|
|
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 =
|
|
94
|
+
var INVOCATIONS_DIR = join2(STATE_DIR, "invocations");
|
|
43
95
|
var INVOCATION_STATE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
44
|
-
var DISPATCH_INTENT_DIR =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 (!
|
|
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
|
|
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
|
|
9
|
-
import {
|
|
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
|
-
|
|
23
|
+
candidates.push(currentDir);
|
|
19
24
|
}
|
|
20
25
|
const parentDir = dirname(currentDir);
|
|
21
26
|
if (parentDir === currentDir) {
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 =
|
|
128
|
+
const dir = dirname2(resolved);
|
|
99
129
|
if (dir === PROJECT_ROOT2) {
|
|
100
130
|
return { decision: "deny", reason: GUIDANCE };
|
|
101
131
|
}
|
package/hooks/post-implement.mjs
CHANGED
|
@@ -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
|
|
6
|
-
import {
|
|
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
|
-
|
|
20
|
+
candidates.push(currentDir);
|
|
16
21
|
}
|
|
17
22
|
const parentDir = dirname(currentDir);
|
|
18
23
|
if (parentDir === currentDir) {
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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";
|