@sabaiway/agent-workflow-kit 5.10.0 → 5.11.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/CHANGELOG.md +152 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +13 -2
- package/references/hooks/state-block-guard.mjs +14 -2
- package/references/modes/commit-guard.md +11 -8
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/dispatch.md +32 -10
- package/references/modes/worktrees.md +47 -3
- package/references/scripts/archive-changelog.mjs +14 -3
- package/references/scripts/archive-decisions.mjs +14 -3
- package/references/scripts/archive-issues.mjs +14 -3
- package/references/scripts/check-docs-size.mjs +14 -3
- package/references/scripts/migrate-gates.mjs +13 -2
- package/tools/ack-write.mjs +3 -3
- package/tools/advisor-matrix.mjs +165 -0
- package/tools/autonomy-doctor.mjs +2 -3
- package/tools/bridge-settings.mjs +2 -3
- package/tools/cheap-agents.mjs +3 -3
- package/tools/commands.mjs +4 -5
- package/tools/commit-guard.mjs +77 -20
- package/tools/core-evidence.mjs +12 -3
- package/tools/coverage-check.mjs +2 -3
- package/tools/delegation.mjs +2 -3
- package/tools/detect-backends.mjs +2 -3
- package/tools/dispatch-advisor.mjs +323 -0
- package/tools/dispatch.mjs +174 -109
- package/tools/doc-parity.mjs +69 -16
- package/tools/family-registry.mjs +3 -3
- package/tools/flow-adoption-mint.mjs +70 -0
- package/tools/flow-append.mjs +309 -0
- package/tools/flow-chain-state.mjs +91 -0
- package/tools/flow-check-cores.mjs +35 -6
- package/tools/flow-check-rungs.mjs +20 -2
- package/tools/flow-check.mjs +22 -8
- package/tools/flow-delta-proof.mjs +307 -0
- package/tools/flow-record.mjs +1 -1
- package/tools/flow-store-read.mjs +3 -3
- package/tools/flow-store.mjs +35 -812
- package/tools/flow-subset-budget.mjs +81 -0
- package/tools/flow-writer.mjs +3 -3
- package/tools/gate-hook.mjs +3 -3
- package/tools/gates-init.mjs +3 -3
- package/tools/grounding.mjs +2 -3
- package/tools/hide-footprint.mjs +2 -3
- package/tools/inject-methodology.mjs +2 -3
- package/tools/lens-region.mjs +2 -3
- package/tools/manifest/validate.mjs +2 -3
- package/tools/migrate-adr-store.mjs +3 -3
- package/tools/observation-builder.mjs +123 -0
- package/tools/path-inventory.mjs +2 -3
- package/tools/procedures.mjs +3 -3
- package/tools/receipt-deadline.mjs +2 -3
- package/tools/recipes.mjs +2 -3
- package/tools/recommendations.mjs +3 -3
- package/tools/release-scan.mjs +2 -3
- package/tools/repo-search.mjs +2 -3
- package/tools/review-state.mjs +3 -3
- package/tools/run-gates.mjs +2 -3
- package/tools/sandbox-masks.mjs +3 -3
- package/tools/satellite-locator.mjs +179 -0
- package/tools/set-autonomy.mjs +2 -3
- package/tools/set-flow.mjs +3 -3
- package/tools/set-recipe.mjs +2 -3
- package/tools/setup-backends.mjs +3 -3
- package/tools/store-append.mjs +2 -2
- package/tools/uninstall.mjs +2 -3
- package/tools/velocity-profile.mjs +3 -3
- package/tools/worktree-handoff-return.mjs +369 -0
- package/tools/worktree-prompt.mjs +190 -0
- package/tools/worktrees-record.mjs +171 -0
- package/tools/worktrees.mjs +311 -300
package/tools/coverage-check.mjs
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
24
24
|
import { createHash } from 'node:crypto';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
|
-
import { pathToFileURL } from 'node:url';
|
|
27
26
|
import { spawnSync } from 'node:child_process';
|
|
28
27
|
import { computeChangedSurface } from './changed-surface.mjs';
|
|
28
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
29
29
|
import { lcovCoveredMap, uncoveredChangedFromLcov } from './lcov.mjs';
|
|
30
30
|
import {
|
|
31
31
|
computeTreeFingerprint,
|
|
@@ -365,8 +365,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
365
365
|
}
|
|
366
366
|
};
|
|
367
367
|
|
|
368
|
-
|
|
369
|
-
if (isDirectRun) {
|
|
368
|
+
if (isDirectRun(import.meta.url)) {
|
|
370
369
|
// The capability is CONSUMED here: snapshot it, then remove it from this process's environment
|
|
371
370
|
// before anything spawns. Every `git` query and every bound-test probe below inherits
|
|
372
371
|
// process.env, so leaving it in place would hand a live attestation context to each of them —
|
package/tools/delegation.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { statSync } from 'node:fs';
|
|
12
12
|
import { join, resolve } from 'node:path';
|
|
13
|
-
import {
|
|
13
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
14
14
|
import { validateManifest, VALID } from './manifest/validate.mjs';
|
|
15
15
|
|
|
16
16
|
// The exact skill name a delegable memory candidate must declare (guards against a wrong-name
|
|
@@ -120,5 +120,4 @@ const main = (argv) => {
|
|
|
120
120
|
console.log(`[delegation] commit gate: ${plan.commitGate} (memory raises its own gate: ${plan.memoryRaisesCommitGate})`);
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
if (isDirectRun) main(process.argv.slice(2));
|
|
123
|
+
if (isDirectRun(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { existsSync, statSync, accessSync, realpathSync, constants } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
-
import { pathToFileURL } from 'node:url';
|
|
25
24
|
import os from 'node:os';
|
|
25
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
26
26
|
import { validateManifest, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
27
27
|
|
|
28
28
|
// Probe states. `unknown` (a wrapped fs error) NEVER counts as present in any readiness rule.
|
|
@@ -439,5 +439,4 @@ const main = (_argv, deps = {}) => {
|
|
|
439
439
|
process.exit(0); // informational, like validate.mjs non-strict — never blocks anything
|
|
440
440
|
};
|
|
441
441
|
|
|
442
|
-
|
|
443
|
-
if (isDirectRun) main(process.argv.slice(2));
|
|
442
|
+
if (isDirectRun(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// dispatch-advisor.mjs — the vehicle-routing ADVISOR (delegation Plan 3, Phase 1; D1-D4).
|
|
2
|
+
//
|
|
3
|
+
// One question, one answer: *which vehicle carries this step class on THIS host, and what has the
|
|
4
|
+
// ledger already recorded for it.* That answer lived only in prose canon and in a remembered rule,
|
|
5
|
+
// so a cold session routed a sub-task from memory or not at all. It is now a printed block at two
|
|
6
|
+
// points of use — `dispatch advise` and the footer of a form-valid `dispatch check`.
|
|
7
|
+
//
|
|
8
|
+
// What it deliberately is NOT:
|
|
9
|
+
// • it never gates. Nothing here refuses a dispatch, and the footer prints ONLY over a form-valid
|
|
10
|
+
// contract, so the checker's exit contract and FIRST line stay byte-identical whatever this
|
|
11
|
+
// module concludes. A choice that diverges from the advice is a NOTE, never a refusal.
|
|
12
|
+
// • THIS MODULE writes nothing and spawns nothing — but the VERB that prints it is not spawn-free,
|
|
13
|
+
// and the honest split is stated in ADVISOR_PROBE_POSTURE below rather than claimed away here.
|
|
14
|
+
// Host capability is filesystem-only: the execute backend through the detector (which spawns
|
|
15
|
+
// nothing by a source-level pin), the cheap vehicles through the PRESENCE of
|
|
16
|
+
// `.claude/agents/<name>.md` at the repository top-level, which the CALLER resolves and declares.
|
|
17
|
+
// • it opens no second ledger door. The recorded history arrives as the CALLER's read outcome —
|
|
18
|
+
// `{ok:true, records}` or `{ok:false, reason}` — and is counted through the store's OWN thread
|
|
19
|
+
// walk (`delegationThreadState`), never a second walker. An unreadable store degrades the
|
|
20
|
+
// history line and leaves the advice standing.
|
|
21
|
+
//
|
|
22
|
+
// Portability is stated rather than assumed: the four bundled vehicles and the kit's own worktrees
|
|
23
|
+
// mode are portable rows; `doc-research` is HOST-LOCAL — a per-host web grant the kit does not
|
|
24
|
+
// bundle — and the host's own subagent lane is ASSUMED/manual, carrying no availability verdict at
|
|
25
|
+
// all. Dependency-free, Node >= 22. No side effects on import; no CLI (the verb lives on dispatch).
|
|
26
|
+
|
|
27
|
+
import { lstatSync } from 'node:fs';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { STEP_CLASSES } from './dispatch-record.mjs';
|
|
30
|
+
import { delegationThreadState } from './dispatch-store.mjs';
|
|
31
|
+
import { AGENTS_DIR, FALLBACK_LENS_ADDITIONAL_ONLY } from './cheap-agents.mjs';
|
|
32
|
+
import { detectBackends, wrapperCmdFor, READY } from './detect-backends.mjs';
|
|
33
|
+
|
|
34
|
+
// The two sentences doc-parity binds into references/modes/dispatch.md. Stated ONCE, here, so the
|
|
35
|
+
// mode doc can never drift into promising a gate this module does not own.
|
|
36
|
+
export const ADVISOR_NO_GATE = 'the advice never gates a dispatch: which vehicle carries a sub-task stays orchestrator judgment, and a divergence from the advice is recorded as a note rather than refused';
|
|
37
|
+
export const HARNESS_SUBAGENT_LANE = "the harness's own subagent lane is ASSUMED/manual — it is not kit-detectable, so it carries no availability verdict here and no acceptance weight";
|
|
38
|
+
|
|
39
|
+
// The other lane, always printed: an unavailable vehicle is not a dead end, it is a degrade you record.
|
|
40
|
+
export const ADVISOR_FALLBACK = 'solo (this orchestrator) — recorded as a degrade, never a silent skip';
|
|
41
|
+
|
|
42
|
+
// The verb's honest posture, bound into the mode doc and interpolated into the tool's HELP so the
|
|
43
|
+
// three surfaces state ONE thing. "Spawns nothing" was false of the VERB from the first line of this
|
|
44
|
+
// lane — the store path is resolved through git unless AW_DELEGATION_STORE names it — so the claim
|
|
45
|
+
// is stated where it is true (the module) and qualified where it is not (the verb).
|
|
46
|
+
export const ADVISOR_PROBE_POSTURE = 'the advisor module itself writes nothing, spawns nothing and opens no second ledger door; the VERB may run read-only git probes — the delegation store path (unless AW_DELEGATION_STORE names it outright) and the repository top-level the cheap vehicles are anchored at — and it never runs a vehicle, a subscription CLI, or anything that writes';
|
|
47
|
+
|
|
48
|
+
// The table's shape, stated ONCE. The header line, the alignment rule, the rendered rows and the
|
|
49
|
+
// parser's arity + per-cell diagnosis all DERIVE from this: three separate statements of one column
|
|
50
|
+
// order is exactly how a reordered header keeps a green gate over rows that no longer mean what
|
|
51
|
+
// their columns say.
|
|
52
|
+
export const ADVISOR_MATRIX_COLUMNS = Object.freeze([
|
|
53
|
+
Object.freeze({ key: 'stepClass', label: 'step class' }),
|
|
54
|
+
Object.freeze({ key: 'vehicle', label: 'vehicle' }),
|
|
55
|
+
Object.freeze({ key: 'availabilityNote', label: 'availability' }),
|
|
56
|
+
Object.freeze({ key: 'returns', label: 'returns' }),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const matrixRow = (cells) => `| ${ADVISOR_MATRIX_COLUMNS.map(({ key }) => cells[key]).join(' | ')} |`;
|
|
60
|
+
|
|
61
|
+
export const ADVISOR_MATRIX_HEADER = matrixRow(Object.fromEntries(ADVISOR_MATRIX_COLUMNS.map(({ key, label }) => [key, label])));
|
|
62
|
+
export const ADVISOR_MATRIX_RULE = matrixRow(Object.fromEntries(ADVISOR_MATRIX_COLUMNS.map(({ key }) => [key, '---'])));
|
|
63
|
+
|
|
64
|
+
// The execute backend is named once; its wrapper cmd is READ from the detector's role registry
|
|
65
|
+
// rather than re-typed, so a renamed wrapper moves this row with it.
|
|
66
|
+
const EXECUTE_BACKEND = 'codex-cli-bridge';
|
|
67
|
+
const EXECUTE_ROLE = 'execute';
|
|
68
|
+
|
|
69
|
+
const BUNDLED = 'bundled vehicle — present once placed in .claude/agents/';
|
|
70
|
+
const HOST_LOCAL = 'HOST-LOCAL — a per-host web grant, never bundled with the kit';
|
|
71
|
+
|
|
72
|
+
// ── the frozen row set: exactly one row per D9 step class ─────────────────────────────────────────
|
|
73
|
+
// Totality is a TEST, not a comment: a class added to dispatch-record.mjs reddens the advisor suite
|
|
74
|
+
// rather than printing nothing at the point of use.
|
|
75
|
+
|
|
76
|
+
const row = (entry) => Object.freeze(entry);
|
|
77
|
+
|
|
78
|
+
export const ADVISOR_ROWS = Object.freeze([
|
|
79
|
+
row({
|
|
80
|
+
stepClass: 'code',
|
|
81
|
+
vehicle: wrapperCmdFor(EXECUTE_BACKEND, EXECUTE_ROLE),
|
|
82
|
+
kind: 'backend',
|
|
83
|
+
backend: EXECUTE_BACKEND,
|
|
84
|
+
portable: true,
|
|
85
|
+
availabilityNote: 'execute backend — readiness read from the bridge install, never from a spawn',
|
|
86
|
+
returns: "a diff plus the wrapper's exec receipt",
|
|
87
|
+
why: 'a bounded code sub-task returns a diff you review + gate',
|
|
88
|
+
}),
|
|
89
|
+
row({
|
|
90
|
+
stepClass: 'extraction',
|
|
91
|
+
vehicle: 'mechanical-sweep',
|
|
92
|
+
kind: 'agent',
|
|
93
|
+
portable: true,
|
|
94
|
+
availabilityNote: BUNDLED,
|
|
95
|
+
returns: 'an extraction report you verify',
|
|
96
|
+
why: 'a mechanical multi-file sweep returns facts, on a read-only vehicle that can never shell out',
|
|
97
|
+
}),
|
|
98
|
+
row({
|
|
99
|
+
stepClass: 'triage',
|
|
100
|
+
vehicle: 'gate-triage',
|
|
101
|
+
kind: 'agent',
|
|
102
|
+
portable: true,
|
|
103
|
+
availabilityNote: BUNDLED,
|
|
104
|
+
returns: 'a structured gate-failure classification',
|
|
105
|
+
why: "a failing gate's output returns classified, never fixed — the fix stays yours",
|
|
106
|
+
}),
|
|
107
|
+
row({
|
|
108
|
+
stepClass: 'draft',
|
|
109
|
+
vehicle: 'changelog-skeleton',
|
|
110
|
+
kind: 'agent',
|
|
111
|
+
portable: true,
|
|
112
|
+
availabilityNote: BUNDLED,
|
|
113
|
+
returns: 'a factual skeleton',
|
|
114
|
+
why: 'the factual bones come back cheap; the lead and the final text stay yours',
|
|
115
|
+
}),
|
|
116
|
+
row({
|
|
117
|
+
stepClass: 'research',
|
|
118
|
+
vehicle: 'doc-research',
|
|
119
|
+
kind: 'agent',
|
|
120
|
+
portable: false,
|
|
121
|
+
availabilityNote: HOST_LOCAL,
|
|
122
|
+
returns: 'cited findings',
|
|
123
|
+
why: 'an external documentation question returns cited findings, on the one vehicle granted web access',
|
|
124
|
+
}),
|
|
125
|
+
row({
|
|
126
|
+
stepClass: 'review-opinion',
|
|
127
|
+
vehicle: 'review-lens',
|
|
128
|
+
kind: 'agent',
|
|
129
|
+
portable: true,
|
|
130
|
+
availabilityNote: BUNDLED,
|
|
131
|
+
returns: 'one additional review opinion',
|
|
132
|
+
why: FALLBACK_LENS_ADDITIONAL_ONLY,
|
|
133
|
+
}),
|
|
134
|
+
row({
|
|
135
|
+
stepClass: 'worktree-stream',
|
|
136
|
+
vehicle: 'worktrees',
|
|
137
|
+
kind: 'kit',
|
|
138
|
+
portable: true,
|
|
139
|
+
availabilityNote: 'ships with the kit — available wherever the kit is deployed',
|
|
140
|
+
returns: 'a prepared satellite diff plus its handoff',
|
|
141
|
+
why: 'a parallel feature stream runs in its own worktree and returns a prepared diff plus its handoff',
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
export const advisorRow = (stepClass) => ADVISOR_ROWS.find((r) => r.stepClass === stepClass);
|
|
146
|
+
|
|
147
|
+
// ── host capability, resolved by filesystem facts only (D2) ───────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
// The agent lane is FOUR-valued, and the two unknowns are kept apart because they are two different
|
|
150
|
+
// ignorances. `.claude/agents/` is a repository-ROOT surface, so a probe run from anywhere else
|
|
151
|
+
// proves nothing — an absent file under an UNANCHORED cwd is not evidence the vehicle is unplaced,
|
|
152
|
+
// and a PRESENT one there is not evidence it is the repository's vehicle either, since a nested
|
|
153
|
+
// shadow copy reads identically. Separately, an anchored probe can simply fail to answer (any errno
|
|
154
|
+
// but ENOENT — EACCES is not absence), and saying "the repository root was not resolved" there would
|
|
155
|
+
// be a false statement about a root that WAS resolved. Both render as `unknown`; each names its own
|
|
156
|
+
// cause.
|
|
157
|
+
export const AGENT_PRESENT = 'present';
|
|
158
|
+
export const AGENT_MISSING = 'missing';
|
|
159
|
+
export const AGENT_UNANCHORED = 'unanchored';
|
|
160
|
+
export const AGENT_PROBE_ERROR = 'probe-error';
|
|
161
|
+
|
|
162
|
+
// The states that are NOT a verdict — exported so a consumer can ask the question without re-deriving
|
|
163
|
+
// which values happen to be ignorance today.
|
|
164
|
+
export const AGENT_UNKNOWN_STATES = Object.freeze([AGENT_UNANCHORED, AGENT_PROBE_ERROR]);
|
|
165
|
+
|
|
166
|
+
// No-follow by construction: the placement writer refuses to write through a symlink, so a symlinked
|
|
167
|
+
// entry is not a vehicle this kit placed and is not counted as one.
|
|
168
|
+
const probeAgentFile = (cwd, name) => {
|
|
169
|
+
try {
|
|
170
|
+
return lstatSync(join(cwd, AGENTS_DIR, `${name}.md`)).isFile() ? AGENT_PRESENT : AGENT_MISSING;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
return err?.code === 'ENOENT' ? AGENT_MISSING : AGENT_PROBE_ERROR;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// advisorDeps({cwd, anchored, detect}) → the injected host half. `anchored` states whether `cwd` IS
|
|
177
|
+
// the repository top-level; when it is not, the agent lane answers `unanchored` rather than guessing.
|
|
178
|
+
// Both probes are pure reads; `detect` is the detector's own no-spawn pass, resolved ONCE per call so
|
|
179
|
+
// seven rows cost one detection.
|
|
180
|
+
export const advisorDeps = ({ cwd = process.cwd(), anchored = true, detect = detectBackends } = {}) => {
|
|
181
|
+
let detected = null;
|
|
182
|
+
return {
|
|
183
|
+
agentState: (name) => (anchored ? probeAgentFile(cwd, name) : AGENT_UNANCHORED),
|
|
184
|
+
backendReadiness: (backend) => {
|
|
185
|
+
detected ??= detect();
|
|
186
|
+
return detected.find((b) => b.name === backend)?.readiness ?? 'not-installed';
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const AGENT_LABELS = Object.freeze({
|
|
192
|
+
portable: Object.freeze({
|
|
193
|
+
[AGENT_PRESENT]: 'ready',
|
|
194
|
+
[AGENT_MISSING]: 'unavailable — not placed; run /agent-workflow-kit agents',
|
|
195
|
+
[AGENT_UNANCHORED]: 'unknown — the repository root was not resolved, so .claude/agents/ was never located',
|
|
196
|
+
[AGENT_PROBE_ERROR]: 'unknown — the repository root resolved, but .claude/agents/ could not be probed there',
|
|
197
|
+
}),
|
|
198
|
+
'host-local': Object.freeze({
|
|
199
|
+
[AGENT_PRESENT]: 'ready — host-local',
|
|
200
|
+
[AGENT_MISSING]: 'unavailable — host-local, not bundled',
|
|
201
|
+
[AGENT_UNANCHORED]: 'unknown — host-local, and the repository root was not resolved',
|
|
202
|
+
[AGENT_PROBE_ERROR]: 'unknown — host-local, and .claude/agents/ could not be probed',
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// The label for a state this module does not recognize. It claims NOTHING — not that the root
|
|
207
|
+
// resolved, not that it did not. Reusing the probe-error wording here would assert "the repository
|
|
208
|
+
// root resolved" about a value that establishes no such thing, which is the same class of false
|
|
209
|
+
// statement the two named unknowns exist to avoid, one level down.
|
|
210
|
+
export const AGENT_UNRECOGNIZED_LABEL = 'unknown — the availability probe returned an unrecognized state, so nothing about this vehicle is established';
|
|
211
|
+
|
|
212
|
+
export const availabilityOf = (entry, deps) => {
|
|
213
|
+
if (entry.kind === 'kit') return { ready: true, label: 'ready — ships with the kit' };
|
|
214
|
+
if (entry.kind === 'backend') {
|
|
215
|
+
const readiness = deps.backendReadiness(entry.backend);
|
|
216
|
+
return readiness === READY
|
|
217
|
+
? { ready: true, label: 'ready' }
|
|
218
|
+
: { ready: false, label: `unavailable — ${readiness}` };
|
|
219
|
+
}
|
|
220
|
+
const state = deps.agentState(entry.vehicle);
|
|
221
|
+
const labels = AGENT_LABELS[entry.portable ? 'portable' : 'host-local'];
|
|
222
|
+
return { ready: state === AGENT_PRESENT, label: labels[state] ?? AGENT_UNRECOGNIZED_LABEL };
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// ── the recorded history: the store's OWN thread walk, over a CLOSED state taxonomy (D4) ──────────
|
|
226
|
+
|
|
227
|
+
const THREAD_KINDS = ['dispatch', 'return', 'fold', 'degrade'];
|
|
228
|
+
|
|
229
|
+
// The four states are exactly what delegationThreadState distinguishes. `open` is printed SEPARATELY
|
|
230
|
+
// and is never among the closed threads: a live thread is not evidence about a finished one.
|
|
231
|
+
const threadStateName = (state) => {
|
|
232
|
+
if (state.closure?.kind === 'fold') return 'folded';
|
|
233
|
+
if (state.closure?.kind === 'degrade') return 'degrade-closed';
|
|
234
|
+
return state.terminal ? 'failure-terminal' : 'open';
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const countThreadStates = (records, stepClass) => {
|
|
238
|
+
const counts = { folded: 0, 'failure-terminal': 0, 'degrade-closed': 0, open: 0 };
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
for (const record of records) {
|
|
241
|
+
if (!THREAD_KINDS.includes(record?.kind)) continue;
|
|
242
|
+
const { nonce } = record;
|
|
243
|
+
// A PRE-DISPATCH degrade carries nonce null and belongs to no thread at all.
|
|
244
|
+
if (typeof nonce !== 'string' || nonce === '' || seen.has(nonce)) continue;
|
|
245
|
+
seen.add(nonce);
|
|
246
|
+
const state = delegationThreadState(records, nonce);
|
|
247
|
+
if (state.dispatch === null || state.dispatch.stepClass !== stepClass) continue;
|
|
248
|
+
counts[threadStateName(state)] += 1;
|
|
249
|
+
}
|
|
250
|
+
return counts;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const CLOSED_ORDER = ['folded', 'failure-terminal', 'degrade-closed'];
|
|
254
|
+
|
|
255
|
+
export const historyLine = (stepClass, ledger) => {
|
|
256
|
+
if (ledger?.ok !== true) {
|
|
257
|
+
return `history: unavailable — ${ledger?.reason ?? 'the ledger read reported no outcome at all'}`;
|
|
258
|
+
}
|
|
259
|
+
const counts = countThreadStates(ledger.records ?? [], stepClass);
|
|
260
|
+
const closed = CLOSED_ORDER.reduce((sum, key) => sum + counts[key], 0);
|
|
261
|
+
if (closed === 0 && counts.open === 0) return 'history: no recorded history';
|
|
262
|
+
const parts = CLOSED_ORDER.filter((key) => counts[key] > 0).map((key) => `${counts[key]} ${key}`);
|
|
263
|
+
const closedClause = closed === 0
|
|
264
|
+
? '0 closed threads'
|
|
265
|
+
: `${closed} closed thread${closed === 1 ? '' : 's'} — ${parts.join(', ')}`;
|
|
266
|
+
return `history: ${closedClause} · ${counts.open} open`;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ── rendering ─────────────────────────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
// renderAdvisorBlock({stepClass, ledger, deps}) → the five-line block, or null for an unknown class.
|
|
272
|
+
// The refusal of an unknown class belongs to the CALLER (a usage exit naming the closed set), not
|
|
273
|
+
// here: this module answers a question and never decides an exit code.
|
|
274
|
+
export const renderAdvisorBlock = ({ stepClass, ledger, deps }) => {
|
|
275
|
+
const entry = advisorRow(stepClass);
|
|
276
|
+
if (entry === undefined) return null;
|
|
277
|
+
const availability = availabilityOf(entry, deps);
|
|
278
|
+
return [
|
|
279
|
+
`dispatch advisor — step class: ${stepClass}`,
|
|
280
|
+
` advice: ${entry.vehicle} (${availability.label}) — ${entry.why}`,
|
|
281
|
+
` fallback: ${ADVISOR_FALLBACK}`,
|
|
282
|
+
` ${historyLine(stepClass, ledger)}`,
|
|
283
|
+
` note: ${ADVISOR_NO_GATE}`,
|
|
284
|
+
].join('\n');
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// renderSelectionNote({stepClass, vehicle}) → the divergence NOTE, or null when the contract's
|
|
288
|
+
// SELECTED vehicle is the advised one. `vehicle.requested` is named only when it differs from
|
|
289
|
+
// `selected` — where the pair agrees there is nothing to distinguish, and printing it twice would
|
|
290
|
+
// read as a second fact.
|
|
291
|
+
export const renderSelectionNote = ({ stepClass, vehicle }) => {
|
|
292
|
+
const entry = advisorRow(stepClass);
|
|
293
|
+
if (entry === undefined || vehicle == null) return null;
|
|
294
|
+
const { requested, selected } = vehicle;
|
|
295
|
+
if (selected === entry.vehicle) return null;
|
|
296
|
+
const requestedClause = requested !== selected ? ` (requested "${requested}")` : '';
|
|
297
|
+
return ` divergence: the contract selected "${selected}"${requestedClause}; the advisor advises "${entry.vehicle}" — a NOTE, never a refusal`;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// The harness lane, as a row rather than as a rendered string: it joins the table through the same
|
|
301
|
+
// column order as every other row, and it is NOT a step class — no availability verdict, no
|
|
302
|
+
// acceptance weight, and it never enters the registry the advice is read from.
|
|
303
|
+
export const HARNESS_LANE_ROW = Object.freeze({
|
|
304
|
+
stepClass: 'harness subagent',
|
|
305
|
+
vehicle: "the host's own",
|
|
306
|
+
availabilityNote: HARNESS_SUBAGENT_LANE,
|
|
307
|
+
returns: 'not measured',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// renderAdvisorMatrix() → the vehicle-routing matrix the mode doc carries, WHOLE: header, alignment
|
|
311
|
+
// rule, one row per registry row in registry order, and the lane. The doc's copy is held to this
|
|
312
|
+
// exact block by doc-parity's structure check, so a deleted rule row, a rewritten lane and an extra
|
|
313
|
+
// row are all caught by the same comparison that catches a drifted cell.
|
|
314
|
+
export const renderAdvisorMatrix = () => [
|
|
315
|
+
ADVISOR_MATRIX_HEADER,
|
|
316
|
+
ADVISOR_MATRIX_RULE,
|
|
317
|
+
...ADVISOR_ROWS.map((entry) => matrixRow({ ...entry, stepClass: `\`${entry.stepClass}\`` })),
|
|
318
|
+
matrixRow(HARNESS_LANE_ROW),
|
|
319
|
+
].join('\n');
|
|
320
|
+
|
|
321
|
+
// The closed class set, rendered for the caller's usage refusal — one source with the record
|
|
322
|
+
// vocabulary, so a class added there is offered here without a second edit.
|
|
323
|
+
export const ADVISOR_STEP_CLASSES = STEP_CLASSES;
|