@sabaiway/agent-workflow-kit 10.5.0 → 11.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/SKILL.md +9 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
  5. package/bridges/antigravity-cli-bridge/capability.json +1 -1
  6. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  7. package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
  8. package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
  9. package/bridges/codex-cli-bridge/capability.json +1 -1
  10. package/capability.json +1 -1
  11. package/package.json +1 -1
  12. package/references/modes/commit-guard.md +2 -2
  13. package/references/modes/control-bytes.md +91 -0
  14. package/references/modes/core-evidence.md +1 -1
  15. package/references/modes/flow-writer.md +16 -4
  16. package/references/modes/procedures.md +5 -1
  17. package/references/modes/review-state.md +3 -3
  18. package/references/modes/robustness-brief.md +61 -0
  19. package/references/modes/velocity.md +2 -0
  20. package/references/robustness-literals.json +107 -0
  21. package/tools/changed-surface.mjs +1 -1
  22. package/tools/commands.mjs +14 -0
  23. package/tools/commit-guard.mjs +24 -12
  24. package/tools/control-bytes.mjs +264 -0
  25. package/tools/core-evidence.mjs +33 -20
  26. package/tools/coverage-check.mjs +1 -1
  27. package/tools/dispatch-store-read.mjs +269 -0
  28. package/tools/dispatch-store.mjs +21 -357
  29. package/tools/flow-check-git-lane.mjs +2 -1
  30. package/tools/flow-delta-proof.mjs +4 -4
  31. package/tools/flow-record-shape.mjs +44 -2
  32. package/tools/flow-round-gates.mjs +151 -0
  33. package/tools/flow-store-read.mjs +1 -1
  34. package/tools/flow-writer.mjs +235 -82
  35. package/tools/fold-scope.mjs +7 -3
  36. package/tools/fs-read-nofollow.mjs +56 -4
  37. package/tools/git-env.mjs +120 -0
  38. package/tools/grounding.mjs +4 -3
  39. package/tools/held-session.mjs +165 -0
  40. package/tools/plan-shape-facts.mjs +23 -10
  41. package/tools/plan-shape.mjs +8 -0
  42. package/tools/procedures.mjs +47 -4
  43. package/tools/repo-lex.mjs +29 -0
  44. package/tools/review-state.mjs +127 -39
  45. package/tools/robustness-brief.mjs +209 -0
  46. package/tools/robustness-literals.mjs +101 -0
  47. package/tools/sandbox-masks.mjs +2 -1
  48. package/tools/set-flow.mjs +2 -1
  49. package/tools/source-size-scope.mjs +19 -25
  50. package/tools/tracked-tree-census.mjs +3 -3
  51. package/tools/velocity-profile.mjs +5 -2
  52. package/tools/worktree-handoff-return.mjs +2 -2
  53. package/tools/worktrees.mjs +2 -2
@@ -0,0 +1,151 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { escapeForDisplay } from './repo-lex.mjs';
3
+ import { SIGNALS, signalFor } from './review-rounds.mjs';
4
+ import { authoritativeFlowRecords, canonicalFlowDigest, flowRecordKey } from './flow-record.mjs';
5
+ import { isNonEmptyString, isPlainObject, refuse } from './flow-vocabulary.mjs';
6
+ import { isShipVerdict } from './core-evidence.mjs';
7
+
8
+ const sha256 = (value) => createHash('sha256').update(value, typeof value === 'string' ? 'utf8' : undefined).digest('hex');
9
+ const item = (text) => ({ text, digest: sha256(text) });
10
+ const linesOf = (payload) => String(payload).split('\n').map((line) => line.replace(/\r$/, ''));
11
+ const codexSchema = (payload) => {
12
+ let decoded;
13
+ try {
14
+ decoded = JSON.parse(payload);
15
+ } catch {
16
+ return null;
17
+ }
18
+ const severities = ['blocker', 'major', 'minor', 'nit'];
19
+ return isPlainObject(decoded)
20
+ && ['ship', 'revise', 'rethink'].includes(decoded.verdict)
21
+ && Array.isArray(decoded.findings)
22
+ && decoded.findings.every((finding) => isPlainObject(finding) && severities.includes(finding.severity))
23
+ ? decoded : null;
24
+ };
25
+
26
+ export const blockingItems = (backend, payload) => {
27
+ if (backend === 'codex') {
28
+ const decoded = codexSchema(payload);
29
+ if (decoded) {
30
+ const selected = decoded.findings.filter(({ severity }) => severity === 'blocker' || severity === 'major');
31
+ if (!selected.every(({ issue }) => isNonEmptyString(issue))) return refuse('codex schema blocking item issue is empty or not a string');
32
+ return { ok: true, mode: 'schema', items: selected.map(({ issue }) => item(issue)) };
33
+ }
34
+ return { ok: true, mode: 'text', items: linesOf(payload).filter((line) => /^\[(blocker|major)\](\s|$)/.test(line)).map(item) };
35
+ }
36
+ if (backend === 'agy') {
37
+ const lines = linesOf(payload);
38
+ const open = lines.findIndex((line) => /^### Blocking\s*$/.test(line));
39
+ const close = lines.findIndex((line, index) => index > open && /^### /.test(line));
40
+ const section = open < 0 ? [] : lines.slice(open + 1, close < 0 ? undefined : close);
41
+ return { ok: true, mode: 'text', items: section.filter((line) => /^[0-9]+[.)]/.test(line)).map(item) };
42
+ }
43
+ return refuse(`cannot enumerate backend ${JSON.stringify(backend)} — no blocking-item vocabulary predicate`);
44
+ };
45
+
46
+ export const findingQuoted = (backend, findings, quote) => findings.includes(quote)
47
+ || (backend === 'codex' && codexSchema(findings)?.findings.some(({ issue }) => issue === quote) === true);
48
+
49
+ const landed = (head, receipts) => head.dispatches
50
+ .filter(({ receiptDigest }) => receiptDigest !== null)
51
+ .map((entry) => ({ entry, receipt: receipts.get(entry.receiptDigest) }))
52
+ .filter(({ receipt }) => receipt !== undefined);
53
+
54
+ export const roundsForSignal = (heads, receipts) => {
55
+ const rounds = heads.map((head) => ({
56
+ fingerprint: head.fingerprint,
57
+ byBackend: Object.fromEntries(landed(head, receipts).map(({ entry, receipt }) => [entry.backend, receipt])),
58
+ }));
59
+ const backends = [...new Set((heads.length ? landed(heads.at(-1), receipts) : []).map(({ entry }) => entry.backend))];
60
+ return {
61
+ rounds,
62
+ obligation: backends.length ? { backends, perBackend: true, minShip: backends.length } : null,
63
+ };
64
+ };
65
+
66
+ const lossName = (read, expected) => {
67
+ if (read?.outcome !== 'ok') return read?.outcome ?? 'error';
68
+ return sha256(read.bytes) === expected ? null : 'swapped';
69
+ };
70
+ const quoteCovered = (ledger, digest) => ledger.some((entry) => entry.findingDigest === digest
71
+ && (['folded', 'rejected', 'escalated'].includes(entry.action)
72
+ || (entry.action === 'queued' && ['claim', 'proofKind', 'proofDigest'].every((key) => key in entry))));
73
+
74
+ export const capIssue = ({ heads, receipts, readManifest }) => {
75
+ const { rounds, obligation } = roundsForSignal(heads, receipts);
76
+ if (obligation === null) return { issue: null, advisories: [] };
77
+ const signal = signalFor(rounds, obligation, '<artifact>');
78
+ if (signal !== SIGNALS.CAP_REACHED && signal !== SIGNALS.CROSSOVER) return { issue: null, advisories: [] };
79
+ const head = heads.at(-1);
80
+ const ledger = head.dispositions;
81
+ const advisories = [];
82
+ const issues = [];
83
+ const uncovered = [];
84
+ for (const { entry, receipt } of landed(head, receipts)) {
85
+ if (ledger.some((disposition) => disposition.action === 'custody-lost' && disposition.receiptDigest === entry.receiptDigest)) continue;
86
+ const needsFallbackDisposition = !isShipVerdict(receipt.verdict) && ledger.length === 0;
87
+ if (!Number.isInteger(receipt.blocking) || receipt.blocking < 0) {
88
+ const why = `blocking ${JSON.stringify(receipt.blocking)} is not a non-negative integer for backend ${JSON.stringify(entry.backend)}`;
89
+ advisories.push(`advisory: ${why}`);
90
+ if (needsFallbackDisposition) issues.push(`enumeration fallback requires at least one disposition in round ${head.round}: ${why}`);
91
+ continue;
92
+ }
93
+ if (receipt.blocking === 0) continue;
94
+ const read = readManifest(entry.backend, entry.dispatchNonce);
95
+ const loss = lossName(read, entry.findingManifestDigest);
96
+ if (loss !== null) {
97
+ const covered = ledger.some((d) => d.action === 'custody-lost' && d.receiptDigest === entry.receiptDigest);
98
+ if (!covered) issues.push(`lost receipt ${entry.receiptDigest} (${loss}); recovery: round-land --dispose custody-lost --receipt ${entry.receiptDigest}`);
99
+ continue;
100
+ }
101
+ const enumerated = blockingItems(entry.backend, read.manifest.findings);
102
+ if (!enumerated.ok || enumerated.items.length !== receipt.blocking) {
103
+ const why = !enumerated.ok ? enumerated.reason : `enumerated count ${enumerated.items.length} differs from blocking ${receipt.blocking} for backend ${JSON.stringify(entry.backend)}`;
104
+ advisories.push(`advisory: ${why}`);
105
+ if (ledger.length === 0) issues.push(`enumeration fallback requires at least one disposition in round ${head.round}: ${why}`);
106
+ continue;
107
+ }
108
+ for (const found of enumerated.items) if (!quoteCovered(ledger, found.digest)) uncovered.push(found);
109
+ }
110
+ if (uncovered.length) {
111
+ issues.push(`uncovered blocking items:\n${uncovered.map(({ digest, text }) => `${digest} ${escapeForDisplay(text)}`).join('\n')}\nrecovery: round-land --dispose <folded|queued|rejected|escalated> --finding <the item text> on the SAME tree`);
112
+ }
113
+ return { issue: issues.length ? issues.join('\n') : null, advisories };
114
+ };
115
+
116
+ export const walkIssue = ({ records, planId, cycle, stepId, round, base, fingerprint }) => {
117
+ if (round <= 1) return null;
118
+ const coordinates = { kind: 'internal-attestation', planId, cycle, stepId, round: round - 1, base, fingerprint };
119
+ const wanted = flowRecordKey(coordinates);
120
+ const found = authoritativeFlowRecords(records).find((record) => flowRecordKey(record) === wanted);
121
+ return found?.walk ? null
122
+ : `round-open round ${round} requires an authoritative walk for round ${round - 1} at {planId=${planId}, cycle=${cycle}, stepId=${stepId}, round=${round - 1}, base=${base}, fingerprint=${fingerprint}}; recovery: internal-attestation ${planId} --walk <file> at the current tree, or --justification <text>`;
123
+ };
124
+
125
+ export const escalationIssue = ({ records, digest, planId, head }) => {
126
+ const override = records.find((record) => record.kind === 'maintainer-override' && canonicalFlowDigest(record) === digest);
127
+ if (!override) return { issue: `override digest ${digest} does not resolve to a maintainer-override`, dispatch: null };
128
+ const chain = records.find((record) => record.kind === 'chain' && canonicalFlowDigest(record) === override.chainRecord);
129
+ if (!chain || chain.planId !== planId) return { issue: `override ${digest} does not bind a chain record of plan ${planId}`, dispatch: null };
130
+ const dispatch = head.dispatches.find(({ receiptDigest }) => receiptDigest === override.vetoReceiptDigest);
131
+ return dispatch
132
+ ? { issue: null, dispatch }
133
+ : { issue: `override ${digest} does not bind a landed receipt of plan ${planId} round ${head.round}`, dispatch: null };
134
+ };
135
+
136
+ export const validateWalk = (walk, coverage) => {
137
+ if (!isPlainObject(walk) || Object.keys(walk).some((key) => !['listVersion', 'rows'].includes(key)) || !Array.isArray(walk.rows)) return refuse('walk must be the closed {listVersion, rows} object');
138
+ if (walk.listVersion !== coverage.listVersion) return refuse(`walk listVersion ${JSON.stringify(walk.listVersion)} does not match shipped listVersion ${coverage.listVersion}`);
139
+ for (const [i, entry] of walk.rows.entries()) {
140
+ if (!isPlainObject(entry) || Object.keys(entry).length !== 3 || !['id', 'class', 'checked'].every((key) => key in entry) || !isNonEmptyString(entry.id) || !isNonEmptyString(entry.class)) return refuse(`walk rows[${i}] must be the closed {id, class, checked} non-empty string object`);
141
+ if (!isNonEmptyString(entry.checked)) return refuse(`walk checked for ${entry.id}:${entry.class} must be a non-empty string`);
142
+ }
143
+ const judged = coverage.rows.filter((row) => !row.deleted && !row.absent);
144
+ for (const row of judged) {
145
+ for (const classId of new Set([...row.tagged, ...row.present])) {
146
+ if (!walk.rows.some((entry) => entry.id === row.id && entry.class === classId && isNonEmptyString(entry.checked))) return refuse(`walk is missing non-empty checked for ${row.id}:${classId}`);
147
+ }
148
+ }
149
+ const uncovered = judged.flatMap((row) => row.present.filter((classId) => !row.tagged.includes(classId)).map((classId) => ({ id: row.id, class: classId })));
150
+ return { ok: true, walk: { listVersion: coverage.listVersion, rows: walk.rows, uncovered } };
151
+ };
@@ -13,6 +13,7 @@ import { join, isAbsolute, normalize, basename, sep } from 'node:path';
13
13
  import { spawnSync } from 'node:child_process';
14
14
  import { validateFlowRecord, authoritativeFlowRecords } from './flow-record.mjs';
15
15
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
16
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
16
17
 
17
18
  export { lstatNoFollowRead, describeNonRegular, readRegularFileNoFollow, readFileBytesNoFollow } from './fs-read-nofollow.mjs';
18
19
 
@@ -23,7 +24,6 @@ export const flowStoreStop = (message) => Object.assign(new Error(`[agent-workfl
23
24
  export const FLOW_STORE_BASENAME = 'agent-workflow-flow.jsonl';
24
25
  export const FLOW_LOCK_SUFFIX = '.lock';
25
26
 
26
- const GIT_MAX_BUFFER = 256 * 1024 * 1024;
27
27
  const gitBuf = (args, cwd) => {
28
28
  const r = spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
29
29
  if (r.error || r.status !== 0) return null;