@sabaiway/agent-workflow-kit 5.3.0 → 5.5.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +2 -1
  3. package/SKILL.md +5 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
  6. package/bridges/antigravity-cli-bridge/capability.json +1 -1
  7. package/bridges/codex-cli-bridge/SKILL.md +53 -5
  8. package/bridges/codex-cli-bridge/bin/codex-exec.sh +622 -30
  9. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +731 -3
  10. package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
  11. package/bridges/codex-cli-bridge/capability.json +15 -10
  12. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
  13. package/capability.json +1 -1
  14. package/package.json +1 -1
  15. package/references/modes/core-evidence.md +1 -1
  16. package/references/modes/coverage-check.md +1 -1
  17. package/references/modes/dispatch.md +29 -0
  18. package/references/modes/gates.md +7 -2
  19. package/references/modes/receipt-deadline.md +3 -3
  20. package/references/modes/recommendations.md +3 -1
  21. package/references/modes/upgrade.md +1 -1
  22. package/references/modes/velocity.md +5 -1
  23. package/references/scripts/migrate-gates.mjs +102 -10
  24. package/references/scripts/migrate-gates.test.mjs +37 -0
  25. package/tools/commands.mjs +7 -0
  26. package/tools/core-evidence.mjs +79 -5
  27. package/tools/coverage-check.mjs +23 -7
  28. package/tools/coverage-producer.mjs +68 -0
  29. package/tools/coverage-state.mjs +24 -0
  30. package/tools/declared-paths.mjs +32 -0
  31. package/tools/detect-backends.mjs +5 -4
  32. package/tools/dispatch-record.mjs +10 -3
  33. package/tools/dispatch-store.mjs +392 -0
  34. package/tools/dispatch.mjs +1779 -0
  35. package/tools/doc-parity.mjs +27 -4
  36. package/tools/exec-producer.mjs +483 -0
  37. package/tools/exec-receipt.mjs +263 -0
  38. package/tools/flow-store.mjs +111 -462
  39. package/tools/gates-declaration.mjs +49 -0
  40. package/tools/gates-init.mjs +83 -6
  41. package/tools/receipt-deadline.mjs +25 -3
  42. package/tools/recommendations.mjs +63 -19
  43. package/tools/release-scan.mjs +33 -0
  44. package/tools/run-gates.mjs +111 -32
  45. package/tools/store-append.mjs +444 -0
  46. package/tools/velocity-profile.mjs +102 -23
@@ -0,0 +1,263 @@
1
+ // exec-receipt.mjs — the wrapper-minted EXEC RECEIPT contract (delegation Plan 2, Phase 1). Pure
2
+ // form: no filesystem, no git, no CLI, no side effects on import. The bridge wrapper MINTS these
3
+ // bytes; `dispatch return` (Phase 2) reads them and absorbs the run into the delegation ledger.
4
+ //
5
+ // Why the artifact exists at all: the bridge is dependency-free bash with NO path to the kit
6
+ // (codex-cli-bridge/capability.json detect.installed resolves the BRIDGE's own directory), so it
7
+ // cannot append to the ledger — a bash-side append would re-implement the lock/CAS leaf and could not
8
+ // run the store's cross-record preflight, a second and drifting legality door. The wrapper therefore
9
+ // mints what it can PROVE about its own run, and the kit absorbs it through the one append door.
10
+ //
11
+ // TWO STATES, one path (D1). `reserved` is written atomically and no-clobber BEFORE the run is spent
12
+ // — that write IS the nonce reservation, so a duplicate nonce refuses before any spend. `terminal`
13
+ // replaces it at exit, written by the run that owns the reservation. The state split is what makes
14
+ // the artifact a safe arrival signal: `terminal` means the report beside it is already complete.
15
+ //
16
+ // The reserved state carries everything knowable PRE-SPEND (wrapperVersion, posture, capS,
17
+ // killGraceS, contractDigest) and NULL in every terminal-only field, so a `--no-receipt` absorb can
18
+ // still source the fields a tree cannot supply. A reserved receipt filling a terminal-only field
19
+ // refuses: the two states are distinguishable by their content, not only by their label.
20
+ //
21
+ // `contractDigest` is computed by the WRAPPER from the dispatch file it was actually handed — an
22
+ // independently produced value, never a copy of what the ledger holds. Without it the store's
23
+ // return↔dispatch correlation would compare the dispatch record against values derived from itself,
24
+ // and a run that executed a DIFFERENT contract would correlate cleanly.
25
+ //
26
+ // The wrapper's outcome vocabulary is a SUBSET of the ledger's (D3): a run can prove only what it
27
+ // observed about itself. `success` = exit 0 with a session id; `missing-identity` = exit 0 without
28
+ // one; `transport-failure` = any nonzero exit, the timeout codes included. Every orchestrator
29
+ // judgment (contract-refusal, partial-edit, acceptance-failure, stale-return, store-failure) is
30
+ // recorded at absorb time, never claimed here.
31
+ //
32
+ // Named grammars are taken by reference, never re-stated: the safe token grammar SAFE_NONCE_RE and
33
+ // the 64-hex digest. Descriptor discipline follows dispatch-record.mjs — own enumerable DATA
34
+ // properties only, each read exactly once, so the bytes a validator approved are the bytes a reader
35
+ // re-reads.
36
+ //
37
+ // Honest limit: a receipt is forgeable, exactly like every other record in this family. What it
38
+ // defends against is a BUGGY or interrupted producer, not a hostile one.
39
+ //
40
+ // Second honest limit, stated where the name is built: the length prefix closes the SEPARATOR
41
+ // ambiguity, not the CASE one. `Codex` and `codex` are both safe tokens and compose different names —
42
+ // but the same FILE on a case-insensitive filesystem, where an atomic no-clobber mint could then
43
+ // refuse a legitimately different dispatch. Closing it means narrowing a grammar the FAMILY owns
44
+ // (SAFE_NONCE_RE lives in flow-record.mjs and is taken by reference here), and the review lane's
45
+ // manifest names carry the identical axis — so it is one family decision, queued as
46
+ // ARTIFACT-BASENAME-NOT-INJECTIVE, not a local fork.
47
+
48
+ import { SAFE_NONCE_RE } from './flow-record.mjs';
49
+
50
+ const refuse = (reason) => ({ ok: false, reason });
51
+ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
52
+ const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
53
+ const isHex64 = (v) => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);
54
+ const isSafeToken = (v) => typeof v === 'string' && SAFE_NONCE_RE.test(v);
55
+
56
+ // The artifact name is LENGTH-PREFIXED, and that is what makes it injective. Both tokens share the
57
+ // safe grammar, which admits `-`, so a plain `<backend>-<nonce>` join is ambiguous: {backend "a-b",
58
+ // nonce "c"} and {backend "a", nonce "b-c"} compose the SAME file, and the no-clobber reservation
59
+ // would then refuse a genuinely different dispatch. Restricting the backend instead was tried and
60
+ // rejected — the ledger's own contract (dispatch-record.mjs) admits any safe token as a backend, so
61
+ // a stricter rule here would record dispatches whose receipts could never be named. The length
62
+ // prefix keeps every character inside the safe set, keeps the name greppable, and recovers the pair:
63
+ // read digits to the first `-`, take that many characters as the backend, the remainder is the nonce.
64
+ const lengthPrefixed = (prefix, backend, nonce, suffix) =>
65
+ (isSafeToken(backend) && isSafeToken(nonce) ? `${prefix}${backend.length}-${backend}-${nonce}${suffix}` : null);
66
+ const isCanonicalInstant = (v) => typeof v === 'string' && Number.isFinite(Date.parse(v)) && new Date(v).toISOString() === v;
67
+ const isByteCount = (v) => Number.isSafeInteger(v) && v >= 0;
68
+
69
+ const isDataProperty = (obj, field) => {
70
+ const descriptor = Object.getOwnPropertyDescriptor(obj, field);
71
+ return descriptor !== undefined && Object.hasOwn(descriptor, 'value');
72
+ };
73
+
74
+ const ACCESSOR_REFUSAL = 'is an ACCESSOR — a receipt field must be a data property, or a re-read could answer differently than the validator did';
75
+
76
+ const short = (v) => {
77
+ let s;
78
+ try {
79
+ s = JSON.stringify(v);
80
+ } catch {
81
+ return `<unserializable ${typeof v}>`;
82
+ }
83
+ if (s === undefined) s = `<${typeof v}>`;
84
+ return s.length > 80 ? `${s.slice(0, 79)}…` : s;
85
+ };
86
+
87
+ const deepFreeze = (value) => {
88
+ if (value !== null && typeof value === 'object') {
89
+ Object.values(value).forEach(deepFreeze);
90
+ Object.freeze(value);
91
+ }
92
+ return value;
93
+ };
94
+
95
+ // ── the closed vocabulary ─────────────────────────────────────────────────────────────────────────
96
+
97
+ export const EXEC_RECEIPT_SCHEMA_VERSION = 1;
98
+ export const EXEC_RECEIPT_KIND = 'exec-receipt';
99
+
100
+ export const EXEC_RECEIPT_STATES = deepFreeze(['reserved', 'terminal']);
101
+
102
+ export const EXEC_RECEIPT_KEYS = deepFreeze([
103
+ 'state', 'backend', 'nonce', 'owner', 'contractDigest', 'wrapperVersion', 'posture',
104
+ 'capS', 'killGraceS', 'sessionId', 'exitStatus', 'outcome', 'reportDigest', 'reportLength', 'timestamp',
105
+ ]);
106
+
107
+ // The fields only a finished run can fill; a reservation carries null in every one of them.
108
+ export const TERMINAL_ONLY_FIELDS = deepFreeze(['sessionId', 'exitStatus', 'outcome', 'reportDigest', 'reportLength']);
109
+
110
+ // D3 — the three outcomes a wrapper can prove about its own run.
111
+ export const WRAPPER_OUTCOMES = deepFreeze(['success', 'transport-failure', 'missing-identity']);
112
+
113
+ const POSTURE_KEYS = deepFreeze(['model', 'effort', 'tier']);
114
+
115
+ const IDENTITY_FIELDS = ['schema', 'kind'];
116
+
117
+ export const EXEC_RECEIPT_BASENAME_PREFIX = 'agent-workflow-exec-receipt-';
118
+ export const EXEC_REPORT_BASENAME_PREFIX = 'agent-workflow-exec-report-';
119
+
120
+ // The {backend, nonce}-derived artifact names. Null when either token leaves the safe grammar — an
121
+ // unsafe token would compose a path, and a name that can escape its directory is never built.
122
+ export const execReceiptBasename = (backend, nonce) =>
123
+ lengthPrefixed(EXEC_RECEIPT_BASENAME_PREFIX, backend, nonce, '.json');
124
+
125
+ export const execReportBasename = (backend, nonce) =>
126
+ lengthPrefixed(EXEC_REPORT_BASENAME_PREFIX, backend, nonce, '.txt');
127
+
128
+ // ── per-field shapes ──────────────────────────────────────────────────────────────────────────────
129
+
130
+ const FIELD_CHECKS = {
131
+ state: { ok: (v) => EXEC_RECEIPT_STATES.includes(v), want: `one of ${EXEC_RECEIPT_STATES.join(' | ')}` },
132
+ backend: { ok: isSafeToken, want: 'a backend name in the safe token grammar ([A-Za-z0-9._-]{1,64})' },
133
+ nonce: { ok: isSafeToken, want: 'a dispatch nonce in the safe token grammar ([A-Za-z0-9._-]{1,64})' },
134
+ owner: { ok: isNonEmptyString, want: 'the non-empty opaque token identifying the run that holds the reservation' },
135
+ contractDigest: { ok: isHex64, want: 'the 64-hex digest the WRAPPER computed from the dispatch file it ran' },
136
+ wrapperVersion: { ok: isNonEmptyString, want: 'the non-empty minting wrapper version' },
137
+ posture: { ok: isPlainObject, want: 'the closed posture object {model, effort, tier}' },
138
+ capS: { ok: (v) => Number.isSafeInteger(v) && v >= 1, want: 'the positive integer wall-clock cap the run ACTUALLY applied' },
139
+ killGraceS: { ok: isByteCount, want: 'the non-negative integer kill grace the run ACTUALLY applied' },
140
+ sessionId: { ok: (v) => v === null || isNonEmptyString(v), want: 'a non-empty backend session id, or null where no session existed' },
141
+ exitStatus: { ok: (v) => v === null || isByteCount(v), want: 'a non-negative integer process exit status, or null on a reservation' },
142
+ outcome: { ok: (v) => v === null || WRAPPER_OUTCOMES.includes(v), want: `one of ${WRAPPER_OUTCOMES.join(' | ')}, or null on a reservation` },
143
+ reportDigest: { ok: (v) => v === null || isHex64(v), want: 'the 64-hex digest of the report artifact, or null when no report was written' },
144
+ reportLength: { ok: (v) => v === null || isByteCount(v), want: 'the non-negative byte length of the report artifact, or null on a reservation' },
145
+ timestamp: { ok: isCanonicalInstant, want: 'a canonical UTC ISO instant (toISOString round-trip)' },
146
+ };
147
+
148
+ const validatePosture = (posture) => {
149
+ if (!isPlainObject(posture)) return refuse('posture must be an object');
150
+ const own = Object.keys(posture);
151
+ const stray = own.find((k) => !POSTURE_KEYS.includes(k));
152
+ if (stray !== undefined) return refuse(`posture: unknown field "${stray}" — the posture key set is closed`);
153
+ const missing = POSTURE_KEYS.find((k) => !own.includes(k));
154
+ if (missing !== undefined) return refuse(`posture: missing field "${missing}"`);
155
+ const accessor = POSTURE_KEYS.find((k) => !isDataProperty(posture, k));
156
+ if (accessor !== undefined) return refuse(`posture: field "${accessor}" ${ACCESSOR_REFUSAL}`);
157
+ if (!isNonEmptyString(posture.model)) return refuse(`posture: model must be a non-empty model name (got ${short(posture.model)})`);
158
+ const bad = ['effort', 'tier'].find((k) => posture[k] !== null && !isNonEmptyString(posture[k]));
159
+ return bad === undefined ? { ok: true } : refuse(`posture: ${bad} must be a non-empty string or null (got ${short(posture[bad])})`);
160
+ };
161
+
162
+ // The state contract, both directions: a reservation proves nothing about a run that has not
163
+ // finished, and a terminal receipt that left a terminal field null would be a reservation wearing the
164
+ // wrong label.
165
+ const validateStateFields = (receipt) => {
166
+ if (receipt.state === 'reserved') {
167
+ const filled = TERMINAL_ONLY_FIELDS.find((f) => receipt[f] !== null);
168
+ return filled === undefined
169
+ ? { ok: true }
170
+ : refuse(`a RESERVED receipt carries null in every terminal-only field — "${filled}" is ${short(receipt[filled])}; a reservation is minted before the run is spent and can prove nothing about its outcome`);
171
+ }
172
+ const empty = ['exitStatus', 'outcome', 'reportLength'].find((f) => receipt[f] === null);
173
+ if (empty !== undefined) {
174
+ return refuse(`a TERMINAL receipt requires "${empty}" — a null there is a reservation wearing the terminal label`);
175
+ }
176
+ return { ok: true };
177
+ };
178
+
179
+ // D3's mapping, enforced as a TOTAL relation so no run can record an outcome its own numbers deny.
180
+ const validateOutcomeMapping = (receipt) => {
181
+ if (receipt.state !== 'terminal') return { ok: true };
182
+ if (receipt.outcome === 'success') {
183
+ if (receipt.exitStatus !== 0) return refuse(`outcome "success" requires exitStatus 0 (got ${receipt.exitStatus}) — a nonzero exit never reports success`);
184
+ if (receipt.sessionId === null) return refuse('outcome "success" requires a non-null sessionId — a run that identified no session is "missing-identity"');
185
+ return { ok: true };
186
+ }
187
+ if (receipt.outcome === 'missing-identity') {
188
+ if (receipt.exitStatus !== 0) return refuse(`outcome "missing-identity" requires exitStatus 0 (got ${receipt.exitStatus}) — a nonzero exit is "transport-failure"`);
189
+ if (receipt.sessionId !== null) return refuse(`outcome "missing-identity" requires sessionId null (got ${short(receipt.sessionId)})`);
190
+ return { ok: true };
191
+ }
192
+ return receipt.exitStatus === 0
193
+ ? refuse('outcome "transport-failure" requires a nonzero exitStatus — a run that exited 0 is "success" or "missing-identity"')
194
+ : { ok: true };
195
+ };
196
+
197
+ // A TERMINAL receipt always has a report behind it — that is the publication ORDER, not a courtesy:
198
+ // the report is written atomically FIRST and the reservation is replaced by the terminal receipt
199
+ // LAST, and a wrapper that cannot complete either write exits nonzero having published no terminal
200
+ // receipt at all. So on `terminal` the digest is REQUIRED, and an empty report stays perfectly
201
+ // expressible as the sha256 of no bytes with reportLength 0. The absent form belongs to the
202
+ // reservation (both fields null), which is exactly what the `--no-receipt` absorb lane reads.
203
+ const validateReportPair = (receipt) => {
204
+ if (receipt.state !== 'terminal') return { ok: true };
205
+ if (receipt.reportDigest !== null) return { ok: true };
206
+ return refuse(`a TERMINAL receipt requires a reportDigest (reportLength ${receipt.reportLength}) — the report is published BEFORE the terminal receipt replaces the reservation, so a terminal artifact with no report behind it is a state no completed run mints; an EMPTY report is the sha256 of no bytes with length 0`);
207
+ };
208
+
209
+ // validateExecReceipt(receipt) → { ok: true } | { ok: false, reason }. Fail closed on an unknown
210
+ // schema/kind/state, a missing, accessor or malformed field, any key outside the closed set, and every
211
+ // cross-field relation the state pins. Never throws on a DATA record.
212
+ export const validateExecReceipt = (receipt) => {
213
+ if (!isPlainObject(receipt)) return refuse('exec receipt is not an object');
214
+ const own = Object.keys(receipt);
215
+ const missingIdentity = IDENTITY_FIELDS.find((f) => !own.includes(f));
216
+ if (missingIdentity !== undefined) return refuse(`missing field "${missingIdentity}" — the identifying fields are read before any value is`);
217
+ const accessorIdentity = IDENTITY_FIELDS.find((f) => !isDataProperty(receipt, f));
218
+ if (accessorIdentity !== undefined) return refuse(`field "${accessorIdentity}" ${ACCESSOR_REFUSAL}`);
219
+ if (receipt.schema !== EXEC_RECEIPT_SCHEMA_VERSION) {
220
+ return refuse(`unknown schema ${short(receipt.schema)} — this reader accepts exec-receipt schema ${EXEC_RECEIPT_SCHEMA_VERSION} only (fail closed)`);
221
+ }
222
+ if (receipt.kind !== EXEC_RECEIPT_KIND) {
223
+ return refuse(`unknown kind ${short(receipt.kind)} — this reader accepts "${EXEC_RECEIPT_KIND}" only (fail closed)`);
224
+ }
225
+ const allowed = [...IDENTITY_FIELDS, ...EXEC_RECEIPT_KEYS];
226
+ const stray = own.find((k) => !allowed.includes(k));
227
+ if (stray !== undefined) return refuse(`unknown field "${stray}" — the exec-receipt key set is closed`);
228
+ for (const field of EXEC_RECEIPT_KEYS) {
229
+ if (!own.includes(field)) return refuse(`missing field "${field}"`);
230
+ if (!isDataProperty(receipt, field)) return refuse(`field "${field}" ${ACCESSOR_REFUSAL}`);
231
+ if (!FIELD_CHECKS[field].ok(receipt[field])) {
232
+ return refuse(`${field} must be ${FIELD_CHECKS[field].want} (got ${short(receipt[field])})`);
233
+ }
234
+ }
235
+ const posture = validatePosture(receipt.posture);
236
+ if (!posture.ok) return posture;
237
+ const state = validateStateFields(receipt);
238
+ if (!state.ok) return state;
239
+ const mapping = validateOutcomeMapping(receipt);
240
+ if (!mapping.ok) return mapping;
241
+ return validateReportPair(receipt);
242
+ };
243
+
244
+ // parseExecReceipt(text) → { ok: true, receipt } | { ok: false, reason }.
245
+ export const parseExecReceipt = (text) => {
246
+ if (typeof text !== 'string') return refuse('exec receipt: the artifact must be text');
247
+ let parsed;
248
+ try {
249
+ parsed = JSON.parse(text);
250
+ } catch {
251
+ return refuse('exec receipt: the artifact is not valid JSON (fail closed)');
252
+ }
253
+ const valid = validateExecReceipt(parsed);
254
+ return valid.ok ? { ok: true, receipt: parsed } : valid;
255
+ };
256
+
257
+ // The wrapper's own mapping, exported so the bridge's minted bytes and the kit's expectation come from
258
+ // ONE rule rather than two implementations that agree today (D3).
259
+ export const wrapperOutcomeFor = (exitStatus, sessionId) => {
260
+ if (!isByteCount(exitStatus)) return null;
261
+ if (exitStatus !== 0) return 'transport-failure';
262
+ return isNonEmptyString(sessionId) ? 'success' : 'missing-identity';
263
+ };