session-orchestrator 4.1.0 → 5.0.0
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/skills/session-plan/SKILL.md +1 -1
- package/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +128 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/commands/ux-grill.md +51 -0
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +117 -50
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +9 -0
- package/docs/components.md +9 -9
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +13 -6
- package/docs/github-mirror-protection.md +61 -20
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/migration-v5.md +62 -0
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +16 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +70 -3
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +401 -16
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +170 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +5 -4
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +151 -8
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +412 -9
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +450 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +27 -71
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +429 -56
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +56 -27
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/mcp-server.sh +16 -1
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +7 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +60 -209
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +8 -4
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +11 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +27 -67
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
- package/skills/session-end/session-metrics-write.md +33 -12
- package/skills/session-plan/SKILL.md +46 -180
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +41 -7
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +32 -127
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
- package/skills/wave-executor/references/wave-loop-review.md +19 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scope-echo.mjs — the RECEIVE-side half of the FILE-SCOPE observability chain (#1092).
|
|
3
|
+
*
|
|
4
|
+
* `hooks/pre-task-scope-disjoint.mjs` observes the SEND side: it emits
|
|
5
|
+
* `orchestrator.wave_dispatch.scope_checked` describing what the guard saw in the
|
|
6
|
+
* prompt the coordinator handed to the dispatch tool. Nothing in this repo can
|
|
7
|
+
* observe the other half — whether the `FILE-SCOPE` block reached the agent's
|
|
8
|
+
* assembled context — because no platform boundary exposes the final prompt
|
|
9
|
+
* (`docs/scope-collision-guard.md` § 4.2).
|
|
10
|
+
*
|
|
11
|
+
* What IS feasible is a self-reported echo: the coordinator appends one
|
|
12
|
+
* instruction line naming the expected digest of the agent's own scope file, and
|
|
13
|
+
* the agent ends its report with `SCOPE-DIGEST: <8-hex>`. Post-wave, the
|
|
14
|
+
* coordinator compares the two and emits
|
|
15
|
+
* `orchestrator.wave_dispatch.scope_echo_checked`.
|
|
16
|
+
*
|
|
17
|
+
* CEILING (BV-004, named deliberately): this is a SOFT signal. The digest is
|
|
18
|
+
* handed to the agent in the prompt, so an agent that never read the scope block
|
|
19
|
+
* can still copy the line — it proves the report carried the digest the
|
|
20
|
+
* coordinator handed it, never that the model read or obeyed the scope. Revisit
|
|
21
|
+
* when the platform exposes a stable prompt-assembly boundary; at that point the
|
|
22
|
+
* digest can be computed against the real assembled prompt instead of echoed.
|
|
23
|
+
*
|
|
24
|
+
* Pure + stdlib only. Nothing here throws on malformed input — a broken echo
|
|
25
|
+
* check must never change a wave's outcome.
|
|
26
|
+
*
|
|
27
|
+
* Exports:
|
|
28
|
+
* scopeDigest(paths) → 8-hex string (deterministic, never throws)
|
|
29
|
+
* renderScopeEchoInstruction(paths) → the ONE prompt line to append
|
|
30
|
+
* extractScopeEcho(reportText) → { echoed, digest }
|
|
31
|
+
* checkScopeEcho({ scopeFilePath, reportText }) → { echoed, match, expected, actual, reason? }
|
|
32
|
+
*
|
|
33
|
+
* CLI:
|
|
34
|
+
* node scripts/lib/scope-echo.mjs --scope-file <path> --instruction
|
|
35
|
+
* node scripts/lib/scope-echo.mjs --scope-file <path> --report-file <path> \
|
|
36
|
+
* [--wave N --agent-id ID --emit]
|
|
37
|
+
* node scripts/lib/scope-echo.mjs --help
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { readFileSync } from 'node:fs';
|
|
41
|
+
import { resolve } from 'node:path';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
|
|
44
|
+
import { digestSha256Short } from './crypto-digest-utils.mjs';
|
|
45
|
+
|
|
46
|
+
/** Marker the agent must emit. Case-sensitive by design — a lowercase lookalike is not an echo. */
|
|
47
|
+
export const SCOPE_ECHO_MARKER = 'SCOPE-DIGEST:';
|
|
48
|
+
|
|
49
|
+
/** Event name for the post-wave verdict. */
|
|
50
|
+
export const SCOPE_ECHO_EVENT = 'orchestrator.wave_dispatch.scope_echo_checked';
|
|
51
|
+
|
|
52
|
+
/** Max characters retained for `agent_id` in the payload (same clamp as the send-side hook). */
|
|
53
|
+
const AGENT_ID_MAX = 120;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The LAST `SCOPE-DIGEST: <8 hex>` occurrence wins. Optional surrounding backticks
|
|
57
|
+
* (agents like to fence the line) and trailing whitespace are tolerated. The
|
|
58
|
+
* negative lookahead is load-bearing: without it `SCOPE-DIGEST: 123456789` would
|
|
59
|
+
* match its first eight characters and report a false echo.
|
|
60
|
+
*/
|
|
61
|
+
const ECHO_RE = /SCOPE-DIGEST:[ \t]*`{0,3}([0-9a-fA-F]{8})(?![0-9a-fA-F])/g;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalize a declared scope into the canonical digest input: trimmed, empties
|
|
65
|
+
* dropped, deduplicated, sorted, joined with `\n`. Order and incidental
|
|
66
|
+
* whitespace therefore never change the digest — two coordinators that wrote the
|
|
67
|
+
* same set of paths in a different order agree.
|
|
68
|
+
*
|
|
69
|
+
* @param {unknown} paths
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
function normalizeScopePaths(paths) {
|
|
73
|
+
if (!Array.isArray(paths)) return [];
|
|
74
|
+
const out = new Set();
|
|
75
|
+
for (const entry of paths) {
|
|
76
|
+
if (typeof entry !== 'string') continue;
|
|
77
|
+
const trimmed = entry.trim();
|
|
78
|
+
if (trimmed) out.add(trimmed);
|
|
79
|
+
}
|
|
80
|
+
return [...out].sort();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Digest of a declared file scope: sha256 over the normalized paths, first 8 hex.
|
|
85
|
+
* An empty (or unusable) scope digests the empty string — deterministic, and it
|
|
86
|
+
* never throws, so a Discovery wave's empty scope has a stable answer rather than
|
|
87
|
+
* an error path.
|
|
88
|
+
*
|
|
89
|
+
* @param {unknown} paths array of path strings (anything else → empty scope).
|
|
90
|
+
* @returns {string} 8-character lowercase hex digest.
|
|
91
|
+
*/
|
|
92
|
+
export function scopeDigest(paths) {
|
|
93
|
+
return digestSha256Short(normalizeScopePaths(paths).join('\n'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The single line the coordinator appends to an agent prompt after the fenced
|
|
98
|
+
* `FILE-SCOPE` block. It names the expected digest outright, so the agent only
|
|
99
|
+
* has to echo it — the check is about the line surviving the round trip, not
|
|
100
|
+
* about making the agent compute a hash.
|
|
101
|
+
*
|
|
102
|
+
* @param {unknown} paths
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
export function renderScopeEchoInstruction(paths) {
|
|
106
|
+
return `End your final report with the line: ${SCOPE_ECHO_MARKER} ${scopeDigest(paths)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Find the LAST scope-echo marker in an agent's report.
|
|
111
|
+
*
|
|
112
|
+
* The MARKER stays case-sensitive by design (a lowercase `scope-digest:` is not
|
|
113
|
+
* an echo), but the HEX PAYLOAD is accepted case-insensitively and normalized to
|
|
114
|
+
* lowercase: `0123ABCD` and `0123abcd` are the same 32 bits, and an agent that
|
|
115
|
+
* upper-cases the digest has demonstrably carried the line through — which is
|
|
116
|
+
* the only thing this signal measures.
|
|
117
|
+
*
|
|
118
|
+
* @param {unknown} reportText
|
|
119
|
+
* @returns {{ echoed: boolean, digest: string|null }}
|
|
120
|
+
*/
|
|
121
|
+
export function extractScopeEcho(reportText) {
|
|
122
|
+
if (typeof reportText !== 'string' || reportText.length === 0) {
|
|
123
|
+
return { echoed: false, digest: null };
|
|
124
|
+
}
|
|
125
|
+
let last = null;
|
|
126
|
+
ECHO_RE.lastIndex = 0;
|
|
127
|
+
for (const match of reportText.matchAll(ECHO_RE)) last = match[1].toLowerCase();
|
|
128
|
+
return last === null ? { echoed: false, digest: null } : { echoed: true, digest: last };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read a per-agent scope file (`<state-dir>/filescopes/wave-<N>/<agent-id>.json`,
|
|
133
|
+
* a JSON array of path strings — shape (a) of the two scope shapes, see
|
|
134
|
+
* CLAUDE.md / AGENTS.md § allowedPaths) and compare its digest against the agent's echo.
|
|
135
|
+
*
|
|
136
|
+
* Never throws: an unreadable or wrongly-shaped scope file yields
|
|
137
|
+
* `{ match: false, reason: 'scope-file-unreadable' }` so the caller logs an
|
|
138
|
+
* informational line instead of failing a wave.
|
|
139
|
+
*
|
|
140
|
+
* An EMPTY declared scope (a Discovery wave, `[]`) is NOT a failed echo: no
|
|
141
|
+
* instruction line was ever injected, so the agent had nothing to echo. Those
|
|
142
|
+
* verdicts carry `applicable: false` + `reason: 'scope-empty'` so a consumer can
|
|
143
|
+
* exclude them from the echo-rate instead of counting every uninstructed agent
|
|
144
|
+
* as a miss. `applicable` is OMITTED (never `true`) on the instructed path — the
|
|
145
|
+
* same "absent is not zero" discipline `wave` follows.
|
|
146
|
+
*
|
|
147
|
+
* @param {{ scopeFilePath?: string, reportText?: string }} args
|
|
148
|
+
* @returns {{ echoed: boolean, match: boolean, expected: string|null, actual: string|null, applicable?: boolean, reason?: string }}
|
|
149
|
+
*/
|
|
150
|
+
export function checkScopeEcho({ scopeFilePath, reportText } = {}) {
|
|
151
|
+
const { echoed, digest } = extractScopeEcho(reportText);
|
|
152
|
+
/** @type {string|null} */
|
|
153
|
+
let expected;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(readFileSync(String(scopeFilePath), 'utf8'));
|
|
156
|
+
if (!Array.isArray(parsed)) throw new TypeError('scope file is not a JSON array');
|
|
157
|
+
if (normalizeScopePaths(parsed).length === 0) {
|
|
158
|
+
return {
|
|
159
|
+
echoed,
|
|
160
|
+
match: false,
|
|
161
|
+
expected: null,
|
|
162
|
+
actual: digest,
|
|
163
|
+
applicable: false,
|
|
164
|
+
reason: 'scope-empty',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
expected = scopeDigest(parsed);
|
|
168
|
+
} catch {
|
|
169
|
+
return { echoed, match: false, expected: null, actual: digest, reason: 'scope-file-unreadable' };
|
|
170
|
+
}
|
|
171
|
+
if (!echoed) return { echoed, match: false, expected, actual: null, reason: 'echo-absent' };
|
|
172
|
+
const match = digest === expected;
|
|
173
|
+
return { echoed, match, expected, actual: digest, ...(match ? {} : { reason: 'digest-mismatch' }) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build the telemetry payload for a verdict. Counts, closed enums and digests
|
|
178
|
+
* only — NO path and NO prompt text (issue #1092 acceptance criterion 3): this
|
|
179
|
+
* record also travels over the optional Clank Event-Bus webhook with no
|
|
180
|
+
* redaction, and declared paths carry private project slugs.
|
|
181
|
+
*
|
|
182
|
+
* NO path here means no path in THIS function's own object literal, before the
|
|
183
|
+
* standard `sessionAttribution` spread, which adds the session ids like every
|
|
184
|
+
* event — that spread is applied by the caller and is out of this promise's
|
|
185
|
+
* scope.
|
|
186
|
+
*
|
|
187
|
+
* `wave` follows the "absent is not zero" rule (`docs/events-schema.md`): it is
|
|
188
|
+
* omitted rather than defaulted when the caller has no wave number. `applicable`
|
|
189
|
+
* follows the same rule in the other direction — it is emitted ONLY as `false`,
|
|
190
|
+
* for a verdict whose agent was never instructed (empty declared scope), so a
|
|
191
|
+
* consumer computing an echo-rate can filter those rows out. Its absence means
|
|
192
|
+
* "instructed", never "unknown". (Events-schema row owned elsewhere: the
|
|
193
|
+
* `orchestrator.wave_dispatch.scope_echo_checked` catalogue entry needs an
|
|
194
|
+
* `applicable` field row.)
|
|
195
|
+
*
|
|
196
|
+
* @param {{ echoed: boolean, match: boolean, expected: string|null, actual: string|null, applicable?: boolean, reason?: string }} verdict
|
|
197
|
+
* @param {{ wave?: unknown, agentId?: unknown }} [meta]
|
|
198
|
+
* @returns {Record<string, unknown>}
|
|
199
|
+
*/
|
|
200
|
+
export function scopeEchoPayload(verdict, meta = {}) {
|
|
201
|
+
const wave = Number(meta.wave);
|
|
202
|
+
const agentId = typeof meta.agentId === 'string' ? meta.agentId.slice(0, AGENT_ID_MAX) : null;
|
|
203
|
+
return {
|
|
204
|
+
...(Number.isFinite(wave) && wave > 0 ? { wave } : {}),
|
|
205
|
+
...(agentId ? { agent_id: agentId } : {}),
|
|
206
|
+
echoed: verdict.echoed,
|
|
207
|
+
match: verdict.match,
|
|
208
|
+
...(verdict.applicable === false ? { applicable: false } : {}),
|
|
209
|
+
expected_digest: verdict.expected ?? null,
|
|
210
|
+
actual_digest: verdict.actual ?? null,
|
|
211
|
+
...(verdict.reason ? { reason: verdict.reason } : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// CLI
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
/** `--help` text. Exit codes are named here per `.claude/rules/cli-design.md` § Discoverability. */
|
|
220
|
+
const USAGE = `Usage:
|
|
221
|
+
scope-echo --scope-file <path> --instruction
|
|
222
|
+
Print the ONE prompt line instructing an agent to echo its scope digest.
|
|
223
|
+
Prints nothing for an empty or unreadable scope file.
|
|
224
|
+
|
|
225
|
+
scope-echo --scope-file <path> [--report-file <path>] [--wave N]
|
|
226
|
+
[--agent-id ID] [--emit]
|
|
227
|
+
Compare the agent report's SCOPE-DIGEST echo against the scope file and
|
|
228
|
+
print the verdict payload as JSON on stdout. --emit additionally appends
|
|
229
|
+
an ${SCOPE_ECHO_EVENT} row to .orchestrator/metrics/events.jsonl
|
|
230
|
+
(best-effort: a failed emit never changes the verdict or the exit code).
|
|
231
|
+
|
|
232
|
+
scope-echo --help
|
|
233
|
+
|
|
234
|
+
Exit codes: 0 for every verdict, 1 for a missing --scope-file.
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Minimal `--flag value` / `--flag` parser (no dependency, same shape as the
|
|
239
|
+
* other `scripts/lib/*` CLIs in this repo).
|
|
240
|
+
*
|
|
241
|
+
* @param {string[]} argv
|
|
242
|
+
* @returns {Record<string, string|boolean>}
|
|
243
|
+
*/
|
|
244
|
+
function parseArgv(argv) {
|
|
245
|
+
/** @type {Record<string, string|boolean>} */
|
|
246
|
+
const out = {};
|
|
247
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
248
|
+
const arg = argv[i];
|
|
249
|
+
if (!arg.startsWith('--')) continue;
|
|
250
|
+
const key = arg.slice(2);
|
|
251
|
+
const next = argv[i + 1];
|
|
252
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
253
|
+
out[key] = next;
|
|
254
|
+
i += 1;
|
|
255
|
+
} else {
|
|
256
|
+
out[key] = true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {string[]} [argv]
|
|
264
|
+
* @returns {Promise<number>} process exit code: 0 for every verdict (this is an
|
|
265
|
+
* observability tool — a broken echo check never fails a wave), or 1 for a
|
|
266
|
+
* missing `--scope-file`, which is a USER error per `.claude/rules/cli-design.md`
|
|
267
|
+
* (1 = user/input error; 2 is reserved for system errors).
|
|
268
|
+
*/
|
|
269
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
270
|
+
const args = parseArgv(argv);
|
|
271
|
+
if (args.help) {
|
|
272
|
+
process.stdout.write(USAGE);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const scopeFilePath = typeof args['scope-file'] === 'string' ? args['scope-file'] : '';
|
|
276
|
+
if (!scopeFilePath) {
|
|
277
|
+
process.stderr.write('scope-echo: --scope-file <path> is required\n');
|
|
278
|
+
process.stderr.write(USAGE);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (args.instruction) {
|
|
283
|
+
let paths = [];
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(readFileSync(scopeFilePath, 'utf8'));
|
|
286
|
+
if (Array.isArray(parsed)) paths = parsed;
|
|
287
|
+
} catch {
|
|
288
|
+
// Unreadable scope file → no instruction. Silent on stdout, because the
|
|
289
|
+
// caller splices stdout straight into the prompt. But it must NOT be
|
|
290
|
+
// silent altogether: a corrupt scope file and a legitimate empty Discovery
|
|
291
|
+
// scope both produced exit 0 with no output at all, so the operator could
|
|
292
|
+
// not tell an injected-nothing-by-design run from a broken one. One stderr
|
|
293
|
+
// line separates them (#1092 review R3).
|
|
294
|
+
process.stderr.write(
|
|
295
|
+
`scope-echo: scope file unreadable (${scopeFilePath}) — no echo line injected\n`,
|
|
296
|
+
);
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
if (paths.length === 0) return 0;
|
|
300
|
+
process.stdout.write(`${renderScopeEchoInstruction(paths)}\n`);
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const reportFile = typeof args['report-file'] === 'string' ? args['report-file'] : '';
|
|
305
|
+
/** @type {string} */
|
|
306
|
+
let reportText;
|
|
307
|
+
try {
|
|
308
|
+
reportText = reportFile ? readFileSync(reportFile, 'utf8') : '';
|
|
309
|
+
} catch {
|
|
310
|
+
reportText = '';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const verdict = checkScopeEcho({ scopeFilePath, reportText });
|
|
314
|
+
const payload = scopeEchoPayload(verdict, {
|
|
315
|
+
wave: args.wave,
|
|
316
|
+
agentId: typeof args['agent-id'] === 'string' ? args['agent-id'] : undefined,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (args.emit) {
|
|
320
|
+
try {
|
|
321
|
+
const repoRoot = process.cwd();
|
|
322
|
+
const { emitEvent, sessionAttribution } = await import('./events.mjs');
|
|
323
|
+
await emitEvent(
|
|
324
|
+
SCOPE_ECHO_EVENT,
|
|
325
|
+
{ ...payload, ...sessionAttribution(repoRoot) },
|
|
326
|
+
{ repoRoot },
|
|
327
|
+
);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
// Telemetry never changes the verdict (same discipline as the send-side
|
|
330
|
+
// hook): an unwritable ledger — a full disk, or `events.jsonl` existing as
|
|
331
|
+
// a DIRECTORY — still prints the verdict on stdout and still exits 0.
|
|
332
|
+
process.stderr.write(`scope-echo: emit failed — ${err?.message ?? err}\n`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const invokedAsCli =
|
|
341
|
+
process.argv[1] !== undefined &&
|
|
342
|
+
resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
343
|
+
|
|
344
|
+
if (invokedAsCli) {
|
|
345
|
+
process.exitCode = await main();
|
|
346
|
+
}
|