@sabaiway/agent-workflow-kit 5.9.0 → 5.11.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/CHANGELOG.md +139 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +32 -11
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.mjs +160 -0
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.test.mjs +235 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +23 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +242 -38
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +482 -38
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +45 -12
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +6 -3
- package/bridges/antigravity-cli-bridge/setup/README.md +18 -5
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/state-block-guard.mjs +107 -45
- 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/set-recipe.md +8 -5
- package/references/modes/state-block-guard.md +39 -31
- package/references/modes/worktrees.md +47 -3
- package/references/scripts/check-docs-size-cli.test.mjs +2 -2
- package/references/shared/report-footer.md +2 -2
- package/references/templates/agent_rules.md +1 -0
- package/tools/advisor-matrix.mjs +165 -0
- package/tools/commands.mjs +2 -2
- package/tools/commit-guard.mjs +74 -17
- package/tools/core-evidence.mjs +10 -0
- package/tools/detect-backends.mjs +1 -0
- package/tools/dispatch-advisor.mjs +323 -0
- package/tools/dispatch.mjs +174 -109
- package/tools/doc-parity.mjs +68 -14
- package/tools/ensure-configs.mjs +4 -4
- package/tools/flow-check-cores.mjs +35 -6
- package/tools/flow-check-rungs.mjs +20 -2
- package/tools/flow-check.mjs +20 -5
- package/tools/lens-region.mjs +13 -1
- package/tools/observation-builder.mjs +123 -0
- package/tools/satellite-locator.mjs +179 -0
- package/tools/source-size-scope.mjs +3 -1
- 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 +308 -297
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// agy-envelope.test.mjs — the envelope parse and the CLI boundary, both driven IN-PROCESS: no
|
|
2
|
+
// subprocess, ever. The parse suites are pure; the CLI suite writes real temporary files under one
|
|
3
|
+
// mkdtemp root, because its failure arms ARE filesystem failures (an unreadable payload, an
|
|
4
|
+
// unwritable destination) and stubbing them would test the stub.
|
|
5
|
+
// Fixtures are INLINE (the packed tarball bans fixtures/ directories) and REDACTED per the plan's
|
|
6
|
+
// Decision 7: no local absolute path, no real conversation id. The payload below is the RECORDED
|
|
7
|
+
// bytes of a live `agy --output-format json` run with only its id replaced, so the test really
|
|
8
|
+
// parses what the CLI printed rather than something this file serialized for itself.
|
|
9
|
+
|
|
10
|
+
import { describe, it, after } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { parseAgyEnvelope, runEnvelopeCli } from './agy-envelope.mjs';
|
|
16
|
+
|
|
17
|
+
const REDACTED_ID = '00000000-1111-2222-3333-444444444444';
|
|
18
|
+
|
|
19
|
+
const RECORDED_RESPONSE = [
|
|
20
|
+
'### Delivery proof',
|
|
21
|
+
'part 1 line 7: alpha-bravo-charlie-delta',
|
|
22
|
+
'Requested addresses, one per line:',
|
|
23
|
+
'part 1 line 7',
|
|
24
|
+
'### Verdict',
|
|
25
|
+
'SHIP — probe run, nothing reviewed.',
|
|
26
|
+
'### Blocking',
|
|
27
|
+
'none',
|
|
28
|
+
'### Non-blocking',
|
|
29
|
+
'none',
|
|
30
|
+
'### Questions',
|
|
31
|
+
'none',
|
|
32
|
+
'',
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
const RECORDED_PAYLOAD = `{"conversation_id":"${REDACTED_ID}","status":"SUCCESS","response":"### Delivery proof\\npart 1 line 7: alpha-bravo-charlie-delta\\nRequested addresses, one per line:\\npart 1 line 7\\n### Verdict\\nSHIP — probe run, nothing reviewed.\\n### Blocking\\nnone\\n### Non-blocking\\nnone\\n### Questions\\nnone\\n","duration_seconds":3.640368748,"num_turns":1,"usage":{"input_tokens":16398,"output_tokens":187,"thinking_tokens":118,"cache_read_tokens":0,"total_tokens":16585}}`;
|
|
36
|
+
|
|
37
|
+
const withFields = (fields) => JSON.stringify({ conversation_id: REDACTED_ID, status: 'SUCCESS', response: 'body', ...fields });
|
|
38
|
+
|
|
39
|
+
describe('agy-envelope — a recorded envelope parses', () => {
|
|
40
|
+
it('yields the response text byte-for-byte and the conversation id', () => {
|
|
41
|
+
const result = parseAgyEnvelope(RECORDED_PAYLOAD, { requireConversationId: true });
|
|
42
|
+
assert.equal(result.ok, true, result.sentence);
|
|
43
|
+
assert.equal(result.response, RECORDED_RESPONSE, 'the model Markdown survives the envelope VERBATIM');
|
|
44
|
+
assert.equal(result.conversationId, REDACTED_ID);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('an EMPTY response is a real answer, not an unreadable envelope (the verdict-less arm owns it)', () => {
|
|
48
|
+
const result = parseAgyEnvelope(withFields({ response: '' }));
|
|
49
|
+
assert.equal(result.ok, true, result.sentence);
|
|
50
|
+
assert.equal(result.response, '');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('agy-envelope — every failure carries its OWN named cause', () => {
|
|
55
|
+
for (const [name, payload, cause] of [
|
|
56
|
+
['empty input', '', 'empty-payload'],
|
|
57
|
+
['a non-JSON blob', 'jetski: no output produced\n', 'not-json'],
|
|
58
|
+
['JSON that is not one object', '[{"status":"SUCCESS","response":"body"}]', 'not-an-envelope'],
|
|
59
|
+
// `null` is a LEGAL JSON document and the obvious failure sentinel at once — it must report
|
|
60
|
+
// what it actually is, or the operator hunts a transport bug that is really a shape bug.
|
|
61
|
+
['the literal JSON document `null`', 'null', 'not-an-envelope'],
|
|
62
|
+
['a bare JSON string', '"just a string"', 'not-an-envelope'],
|
|
63
|
+
['a bare JSON number', '42', 'not-an-envelope'],
|
|
64
|
+
['valid JSON with no response field', '{"conversation_id":"x","status":"SUCCESS"}', 'response'],
|
|
65
|
+
['a non-string response', withFields({ response: 42 }), 'response'],
|
|
66
|
+
['a non-SUCCESS status', withFields({ status: 'ERROR' }), 'status'],
|
|
67
|
+
['an absent status', '{"response":"body"}', 'status'],
|
|
68
|
+
]) {
|
|
69
|
+
it(`${name} → cause "${cause}"`, () => {
|
|
70
|
+
const result = parseAgyEnvelope(payload);
|
|
71
|
+
assert.equal(result.ok, false, `${name} must not parse`);
|
|
72
|
+
assert.equal(result.cause, cause);
|
|
73
|
+
assert.ok(result.sentence.length > 0, 'a cause without a sentence tells the operator nothing');
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// The id is required ONLY when the caller declares it will route a later turn at this conversation.
|
|
79
|
+
// Failing a single-turn review over a field it never uses would be a refusal with no defect behind it.
|
|
80
|
+
describe('agy-envelope — the conversation id is validated exactly when it is needed', () => {
|
|
81
|
+
for (const [name, payload] of [
|
|
82
|
+
['an absent id', '{"status":"SUCCESS","response":"body"}'],
|
|
83
|
+
['a non-string id', withFields({ conversation_id: 12345 })],
|
|
84
|
+
['an id failing the UUID grammar', withFields({ conversation_id: 'not-a-uuid' })],
|
|
85
|
+
['a TRUNCATED uuid', withFields({ conversation_id: '00000000-1111-2222-3333-4444444444' })],
|
|
86
|
+
]) {
|
|
87
|
+
it(`${name} fails with cause "conversation-id" when routing is required`, () => {
|
|
88
|
+
const result = parseAgyEnvelope(payload, { requireConversationId: true });
|
|
89
|
+
assert.equal(result.ok, false);
|
|
90
|
+
assert.equal(result.cause, 'conversation-id');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it(`${name} parses fine when routing is NOT required`, () => {
|
|
94
|
+
const result = parseAgyEnvelope(payload);
|
|
95
|
+
assert.equal(result.ok, true, result.sentence);
|
|
96
|
+
assert.equal(result.response, 'body');
|
|
97
|
+
assert.equal(result.conversationId, '', 'an unrequested id is never handed back as if it were validated');
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── evidence: the plan's REJECTED and DEFERRED decisions stay checkable ──────────────────────────
|
|
103
|
+
// STRUCTURAL EXCERPTS of two recorded probes (redacted). Nothing here is claimed byte-complete —
|
|
104
|
+
// only the fields the Appendix really recorded are asserted.
|
|
105
|
+
|
|
106
|
+
// Probe C: probe A's prompt with a schema ON — the matched control for the schema-cost claim.
|
|
107
|
+
const SCHEMA_RUN_RESULT = {
|
|
108
|
+
conversation_id: REDACTED_ID,
|
|
109
|
+
status: 'SUCCESS',
|
|
110
|
+
response: 'the full prose review, with the structured JSON appended as trailing text',
|
|
111
|
+
duration_seconds: 6.718974944,
|
|
112
|
+
num_turns: 2,
|
|
113
|
+
usage: { input_tokens: 33165, output_tokens: 281, thinking_tokens: 185, cache_read_tokens: 0, total_tokens: 33446 },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// The stream-json `init` event of the same run — the ONLY place the ACTUALLY resolved model appears.
|
|
117
|
+
const STREAM_INIT_EVENT = {
|
|
118
|
+
event: 'init',
|
|
119
|
+
conversation_id: REDACTED_ID,
|
|
120
|
+
init: { model: 'Gemini 3.7 Flash (High)', cwd: '<redacted local path>', permission_mode: 'request-review' },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ── the CLI boundary, driven IN-PROCESS ──────────────────────────────────────────────────────────
|
|
124
|
+
// Every CLI arm RETURNS its outcome, so each one is exercised here rather than through a spawn.
|
|
125
|
+
// That is not a convenience: coverage is collected in the test process, so an arm reachable only
|
|
126
|
+
// through a subprocess is an arm nothing can prove was ever run. Real temp files, no subprocess.
|
|
127
|
+
const CLI_ROOT = mkdtempSync(join(tmpdir(), 'agy-envelope-cli-'));
|
|
128
|
+
after(() => rmSync(CLI_ROOT, { recursive: true, force: true }));
|
|
129
|
+
|
|
130
|
+
const cliCase = (name, payload) => {
|
|
131
|
+
const dir = join(CLI_ROOT, name);
|
|
132
|
+
mkdirSync(dir, { recursive: true });
|
|
133
|
+
const envelope = join(dir, 'envelope.json');
|
|
134
|
+
writeFileSync(envelope, payload);
|
|
135
|
+
return { envelope, responseOut: join(dir, 'response.txt'), conversationIdOut: join(dir, 'conv.txt'), dir };
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
describe('agy-envelope — the CLI boundary returns an outcome for every arm', () => {
|
|
139
|
+
it('a complete invocation writes the response VERBATIM and the conversation id, exit 0', () => {
|
|
140
|
+
const c = cliCase('ok', RECORDED_PAYLOAD);
|
|
141
|
+
const outcome = runEnvelopeCli(['--envelope', c.envelope, '--response-out', c.responseOut, '--conversation-id-out', c.conversationIdOut]);
|
|
142
|
+
assert.equal(outcome.code, 0, outcome.message);
|
|
143
|
+
assert.equal(readFileSync(c.responseOut, 'utf8'), RECORDED_RESPONSE);
|
|
144
|
+
assert.equal(readFileSync(c.conversationIdOut, 'utf8'), REDACTED_ID);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('without --conversation-id-out no id file is written and the id is never required', () => {
|
|
148
|
+
const c = cliCase('no-id', withFields({ conversation_id: 'not-a-uuid' }));
|
|
149
|
+
const outcome = runEnvelopeCli(['--envelope', c.envelope, '--response-out', c.responseOut]);
|
|
150
|
+
assert.equal(outcome.code, 0, outcome.message);
|
|
151
|
+
assert.equal(readFileSync(c.responseOut, 'utf8'), 'body');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
for (const [name, argv, fragment] of [
|
|
155
|
+
['an unknown argument', ['--bogus', 'x'], "unknown argument '--bogus'"],
|
|
156
|
+
['a flag with no value', ['--envelope'], '--envelope needs a value'],
|
|
157
|
+
['a flag swallowing the next flag', ['--envelope', '--response-out'], '--envelope needs a value'],
|
|
158
|
+
['a missing --envelope', ['--response-out', '/dev/null'], '--envelope is required'],
|
|
159
|
+
['a missing --response-out', ['--envelope', '/dev/null'], '--response-out is required'],
|
|
160
|
+
]) {
|
|
161
|
+
it(`${name} is a USAGE refusal (exit 2) carrying the usage text`, () => {
|
|
162
|
+
const outcome = runEnvelopeCli(argv);
|
|
163
|
+
assert.equal(outcome.code, 2, `${name}: ${outcome.message}`);
|
|
164
|
+
assert.ok(outcome.message.includes(fragment), `${name}: ${outcome.message}`);
|
|
165
|
+
assert.ok(outcome.message.includes('Usage:'), 'a usage refusal prints the usage');
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
it('an unreadable payload (a DIRECTORY at the path) is the named unreadable-payload cause', () => {
|
|
170
|
+
const c = cliCase('unreadable', '{}');
|
|
171
|
+
const outcome = runEnvelopeCli(['--envelope', c.dir, '--response-out', c.responseOut]);
|
|
172
|
+
assert.equal(outcome.code, 1, outcome.message);
|
|
173
|
+
assert.ok(outcome.message.includes('unreadable-payload'), outcome.message);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('a payload that is not valid UTF-8 is the named not-utf8 cause', () => {
|
|
177
|
+
const dir = join(CLI_ROOT, 'not-utf8');
|
|
178
|
+
mkdirSync(dir, { recursive: true });
|
|
179
|
+
const envelope = join(dir, 'envelope.json');
|
|
180
|
+
writeFileSync(envelope, Buffer.from([0x7b, 0xff, 0xfe, 0x7d]));
|
|
181
|
+
const outcome = runEnvelopeCli(['--envelope', envelope, '--response-out', join(dir, 'r.txt')]);
|
|
182
|
+
assert.equal(outcome.code, 1, outcome.message);
|
|
183
|
+
assert.ok(outcome.message.includes('not-utf8'), outcome.message);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('a parse failure rides out as its own named cause (exit 1), not as usage', () => {
|
|
187
|
+
const c = cliCase('bad-json', 'not json at all');
|
|
188
|
+
const outcome = runEnvelopeCli(['--envelope', c.envelope, '--response-out', c.responseOut]);
|
|
189
|
+
assert.equal(outcome.code, 1, outcome.message);
|
|
190
|
+
assert.ok(outcome.message.includes('not-json'), outcome.message);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('an unwritable response destination is the named write-failed cause', () => {
|
|
194
|
+
const c = cliCase('unwritable-response', RECORDED_PAYLOAD);
|
|
195
|
+
const outcome = runEnvelopeCli(['--envelope', c.envelope, '--response-out', join(c.dir, 'no-such-dir', 'r.txt')]);
|
|
196
|
+
assert.equal(outcome.code, 1, outcome.message);
|
|
197
|
+
assert.ok(outcome.message.includes('write-failed'), outcome.message);
|
|
198
|
+
assert.ok(outcome.message.includes('response'), 'the failing field is named');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('an unwritable conversation-id destination is named too — the response write already succeeded', () => {
|
|
202
|
+
const c = cliCase('unwritable-id', RECORDED_PAYLOAD);
|
|
203
|
+
const outcome = runEnvelopeCli([
|
|
204
|
+
'--envelope', c.envelope, '--response-out', c.responseOut,
|
|
205
|
+
'--conversation-id-out', join(c.dir, 'no-such-dir', 'conv.txt'),
|
|
206
|
+
]);
|
|
207
|
+
assert.equal(outcome.code, 1, outcome.message);
|
|
208
|
+
assert.ok(outcome.message.includes('conversation_id'), outcome.message);
|
|
209
|
+
assert.equal(readFileSync(c.responseOut, 'utf8'), RECORDED_RESPONSE, 'the response was already written');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('agy-envelope — recorded evidence for the rejected and deferred decisions', () => {
|
|
214
|
+
it('--json-schema is REJECTED: it buys a SECOND billed turn, not a constrained decode', () => {
|
|
215
|
+
const plain = JSON.parse(RECORDED_PAYLOAD);
|
|
216
|
+
assert.equal(plain.num_turns, 1, 'the plain envelope answers in ONE turn');
|
|
217
|
+
assert.equal(SCHEMA_RUN_RESULT.num_turns, 2, 'the schema run answers in TWO — the model restates its own prose');
|
|
218
|
+
assert.ok(
|
|
219
|
+
SCHEMA_RUN_RESULT.usage.total_tokens > 2 * plain.usage.total_tokens,
|
|
220
|
+
`matched control: ${plain.usage.total_tokens} tokens without a schema against ${SCHEMA_RUN_RESULT.usage.total_tokens} with one`,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('stream-json is DEFERRED for a real gain: the RESOLVED model rides `init` and nothing else', () => {
|
|
225
|
+
assert.equal(typeof STREAM_INIT_EVENT.init.model, 'string', 'the init event names the model that actually answered');
|
|
226
|
+
assert.ok(!Object.hasOwn(JSON.parse(RECORDED_PAYLOAD), 'model'), 'the plain envelope carries no resolved model');
|
|
227
|
+
assert.ok(!Object.hasOwn(SCHEMA_RUN_RESULT, 'model'), 'and neither does the stream-json result event');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('the parse ignores the envelope fields it does not need — an added CLI field is not a breakage', () => {
|
|
231
|
+
const result = parseAgyEnvelope(JSON.stringify({ ...SCHEMA_RUN_RESULT, structured_output: { verdict: 'REWORK' } }));
|
|
232
|
+
assert.equal(result.ok, true, result.sentence);
|
|
233
|
+
assert.equal(result.response, SCHEMA_RUN_RESULT.response);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -16,11 +16,33 @@ import { spawnSync } from 'node:child_process';
|
|
|
16
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const WRAPPER = join(HERE, 'agy-review.sh');
|
|
18
18
|
|
|
19
|
+
// Same shape as the sibling spec's fake, minimally: --help / --version answer the wrapper's
|
|
20
|
+
// pre-spend capability door WITHOUT touching the invocation sentinel (a probe is not a paid
|
|
21
|
+
// dispatch), and the answer leaves as an `--output-format json` envelope when the dispatch asked
|
|
22
|
+
// for one — the model's text rides `response` VERBATIM.
|
|
23
|
+
const FAKE_ENVELOPE_ENCODER = [
|
|
24
|
+
'const text = require("node:fs").readFileSync(0, "utf8");',
|
|
25
|
+
'const envelope = { conversation_id: "11111111-2222-3333-4444-555555555555", status: "SUCCESS",',
|
|
26
|
+
' response: text, duration_seconds: 1.5, num_turns: 1,',
|
|
27
|
+
' usage: { input_tokens: 10, output_tokens: 5, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 15 } };',
|
|
28
|
+
'process.stdout.write(`${JSON.stringify(envelope)}\\n`);',
|
|
29
|
+
].join('\n');
|
|
30
|
+
|
|
19
31
|
const FAKE_AGY = [
|
|
20
32
|
'#!/usr/bin/env bash',
|
|
21
33
|
'set -u',
|
|
34
|
+
'case "${1:-}" in',
|
|
35
|
+
' --help|-h) printf " --output-format\\n --disable-slash-commands\\n"; exit 0 ;;',
|
|
36
|
+
' --version) printf "1.1.13\\n"; exit 0 ;;',
|
|
37
|
+
'esac',
|
|
22
38
|
'printf invoked > "${AGY_FAKE_SENTINEL:-/dev/null}"',
|
|
23
|
-
'
|
|
39
|
+
'aw_fmt=""',
|
|
40
|
+
'prev=""; for a in "$@"; do if [[ "$prev" == "--output-format" ]]; then aw_fmt="$a"; fi; prev="$a"; done',
|
|
41
|
+
'if [[ "$aw_fmt" == "json" ]]; then',
|
|
42
|
+
` printf "%s\\n" "\${AGY_FAKE_OUTPUT:-### Verdict}" | node -e '${FAKE_ENVELOPE_ENCODER}'`,
|
|
43
|
+
'else',
|
|
44
|
+
' printf "%s\\n" "${AGY_FAKE_OUTPUT:-### Verdict}"',
|
|
45
|
+
'fi',
|
|
24
46
|
'exit 0',
|
|
25
47
|
'',
|
|
26
48
|
].join('\n');
|
|
@@ -80,6 +80,18 @@ Grounding:
|
|
|
80
80
|
AGY_PROBE=1); plan/diff proceed with a loud warning
|
|
81
81
|
|
|
82
82
|
Notes:
|
|
83
|
+
transport: every review dispatch drives the CLI in --output-format json (plus
|
|
84
|
+
--disable-slash-commands) and the returned envelope is parsed in node (bin/agy-envelope.mjs) — the
|
|
85
|
+
operator-facing invocations and flags above do NOT change, and on a ZERO exit the wrapper still
|
|
86
|
+
PRINTS the review text, never JSON. A missing or unreadable envelope on a zero exit is a loud
|
|
87
|
+
failure with NO receipt, never a downgraded verdict and never a fallback to raw-stdout parsing; a
|
|
88
|
+
non-zero CLI exit keeps its own code and message, and publishes the captured stdout unchanged from
|
|
89
|
+
the SINGLE dispatch or the FINAL fed turn (which may therefore be a JSON or partial payload — the
|
|
90
|
+
envelope is parsed only on a zero exit); an INTERMEDIATE feed turn is the exception, its output
|
|
91
|
+
stays private (Invariant E) and its failure prints only a named error. Enforced by a PRE-SPEND
|
|
92
|
+
capability probe, not a version floor: agy --help must advertise --output-format and
|
|
93
|
+
--disable-slash-commands, node must be >= 22, and bin/agy-envelope.mjs must be present — otherwise
|
|
94
|
+
the review refuses before any run is spent and names the missing capability
|
|
83
95
|
pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts
|
|
84
96
|
against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE
|
|
85
97
|
dispatching, never fired into a known prompt
|
|
@@ -326,7 +338,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.7 Flash (High)"
|
|
|
326
338
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
327
339
|
# version (drift-guarded by agy-review.test.mjs against capability.json).
|
|
328
340
|
AW_RECEIPT_BACKEND="agy"
|
|
329
|
-
AW_BRIDGE_VERSION="5.
|
|
341
|
+
AW_BRIDGE_VERSION="5.3.0" # aw-version-anchor
|
|
330
342
|
# `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
|
|
331
343
|
AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
|
|
332
344
|
# D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
|
|
@@ -455,7 +467,68 @@ FED_ECHO_MAX_BYTES=200
|
|
|
455
467
|
# address nobody requested and FAILS the proof, rather than vanishing from the check entirely.
|
|
456
468
|
FED_PROOF_ADDRESS_MAX=1000000000
|
|
457
469
|
|
|
458
|
-
|
|
470
|
+
# This wrapper's REAL directory, symlinks RESOLVED. The managed install puts a symlink on PATH
|
|
471
|
+
# (~/.local/bin/agy-review -> <placed skill>/bin/agy-review.sh), so an unresolved BASH_SOURCE names
|
|
472
|
+
# the LINK's directory — and every sibling payload below, the envelope reader most of all, would be
|
|
473
|
+
# looked for beside the link instead of beside the script. `readlink -f` is GNU-only (macOS ships
|
|
474
|
+
# BSD readlink), so the chain is walked by hand; `cd -P` resolves a symlinked directory too.
|
|
475
|
+
# The walk is HOP-BOUNDED with a loud refusal, never an unbounded loop. A symlink CYCLE cannot be
|
|
476
|
+
# reached by construction — the kernel already resolved this path to exec the script and would have
|
|
477
|
+
# failed with ELOOP — but it becomes reachable if a link is rewritten in the window between exec and
|
|
478
|
+
# this walk, and an unbounded `while` would then spin until something killed it. Past the bound the
|
|
479
|
+
# wrapper refuses BY NAME rather than guessing at a directory: a wrong answer here sends every
|
|
480
|
+
# sibling lookup somewhere arbitrary. 16 is far above any managed install (which uses ONE hop) and
|
|
481
|
+
# below the kernel's own chain limit, so it never truncates a real chain.
|
|
482
|
+
AW_SCRIPT_LINK_MAX_HOPS=16
|
|
483
|
+
aw_script_dir() {
|
|
484
|
+
local src="${BASH_SOURCE[0]}" dir hops=0
|
|
485
|
+
while [[ -L "$src" ]]; do
|
|
486
|
+
if (( hops >= AW_SCRIPT_LINK_MAX_HOPS )); then
|
|
487
|
+
echo "error: resolving this wrapper's own path exceeded ${AW_SCRIPT_LINK_MAX_HOPS} symlink hops at" >&2
|
|
488
|
+
echo " '$src' — a symlink cycle, or an absurd chain." >&2
|
|
489
|
+
return 1
|
|
490
|
+
fi
|
|
491
|
+
hops=$(( hops + 1 ))
|
|
492
|
+
dir="$(cd -P "$(dirname "$src")" && pwd)" || return 1
|
|
493
|
+
src="$(readlink "$src")" || return 1
|
|
494
|
+
case "$src" in /*) ;; *) src="$dir/$src" ;; esac
|
|
495
|
+
done
|
|
496
|
+
( cd -P "$(dirname "$src")" && pwd )
|
|
497
|
+
}
|
|
498
|
+
if ! HERE="$(aw_script_dir)"; then
|
|
499
|
+
echo "error: this wrapper could not resolve its OWN directory, so the sibling payload every review" >&2
|
|
500
|
+
echo " depends on cannot be located. Refusing before anything is spent — repair the link, or" >&2
|
|
501
|
+
echo " invoke the real script path." >&2
|
|
502
|
+
exit 127
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
# --- The dispatch transport (AGY-1.1.13, Decision 1) ---------------------------
|
|
506
|
+
# EVERY review dispatch runs `--output-format json`: the CLI then answers with ONE envelope whose
|
|
507
|
+
# `response` carries the model's Markdown VERBATIM, so the wrapper reads a NAMED FIELD instead of
|
|
508
|
+
# guessing at raw stdout. The review CONTRACT is untouched — the model is asked for exactly what it
|
|
509
|
+
# was asked for before; only the wrapper's reading of the answer moved.
|
|
510
|
+
# `--disable-slash-commands` rides the same array: a change set is delivered as prompt TEXT, and a
|
|
511
|
+
# body line that happens to begin with a slash command is body — never an instruction for the CLI to
|
|
512
|
+
# expand. It is also what makes the pre-spend door below honest: that door refuses an install whose
|
|
513
|
+
# --help does not advertise this flag, and a door may never require a capability the wrapper leaves
|
|
514
|
+
# unused.
|
|
515
|
+
AGY_TRANSPORT_FLAGS=(--output-format json --disable-slash-commands)
|
|
516
|
+
AGY_ENVELOPE_READER="$HERE/agy-envelope.mjs"
|
|
517
|
+
# A captured answer that is not a readable envelope on a ZERO exit (Decision 3). Its own code, not
|
|
518
|
+
# the D4 verdict-less 4: those are different failures and an operator must be able to tell them
|
|
519
|
+
# apart — D4 means the model answered without a verdict, this means the CLI's answer never arrived
|
|
520
|
+
# in the shape this wrapper reads.
|
|
521
|
+
AGY_ENVELOPE_EXIT=5
|
|
522
|
+
|
|
523
|
+
# Read ONE captured envelope through the sibling module. This PRINTS NOTHING on success — the caller
|
|
524
|
+
# decides what reaches stdout, which is exactly what keeps Invariant E intact when a feed turn's
|
|
525
|
+
# envelope has to be validated without ever becoming operator-visible. A failure is loud and carries
|
|
526
|
+
# the module's own named cause on stderr.
|
|
527
|
+
read_agy_envelope() { # $1 = captured payload, $2 = response destination, $3 = "" | conversation-id destination
|
|
528
|
+
local args=(--envelope "$1" --response-out "$2")
|
|
529
|
+
if [[ -n "${3:-}" ]]; then args+=(--conversation-id-out "$3"); fi
|
|
530
|
+
node "$AGY_ENVELOPE_READER" "${args[@]}"
|
|
531
|
+
}
|
|
459
532
|
|
|
460
533
|
# --- Subscription invariant (reuse agy.sh's security pattern verbatim) --------
|
|
461
534
|
export PATH="$HOME/.local/bin:$PATH"
|
|
@@ -499,6 +572,106 @@ if ! grep -q "AGY_REQUIRE_TIMEOUT_BIN" "$aw_child_path" 2>/dev/null; then
|
|
|
499
572
|
exit 127
|
|
500
573
|
fi
|
|
501
574
|
|
|
575
|
+
# --- Pre-spend capability door (AGY-1.1.13, Decision 2) ------------------------
|
|
576
|
+
# This wrapper drives the CLI in `--output-format json`, passes --disable-slash-commands, and reads
|
|
577
|
+
# the returned envelope in node. A host that cannot do all three must refuse BEFORE a subscription
|
|
578
|
+
# turn is spent — never after a paid run returns something unreadable.
|
|
579
|
+
# A version FLOOR is deliberately NOT the door: the release that introduced --output-format is not
|
|
580
|
+
# measurable from one installed build, so a guessed floor would refuse working installs. The door
|
|
581
|
+
# probes the CAPABILITY the dispatch actually uses instead — the flags `agy --help` advertises, plus
|
|
582
|
+
# a usable node. A probe that FAILS is never read as "capability present" (fail closed).
|
|
583
|
+
AW_REQUIRED_AGY_FLAGS=(--output-format --disable-slash-commands)
|
|
584
|
+
AW_MIN_NODE_MAJOR=22
|
|
585
|
+
aw_agy_version() {
|
|
586
|
+
local v
|
|
587
|
+
v="$(agy --version 2>/dev/null | LC_ALL=C awk 'NR == 1 { print }' || true)"
|
|
588
|
+
printf '%s' "${v:-unknown}"
|
|
589
|
+
}
|
|
590
|
+
set +e
|
|
591
|
+
aw_help_probe="$(agy --help 2>&1)"
|
|
592
|
+
aw_help_rc=$?
|
|
593
|
+
set -e
|
|
594
|
+
if (( aw_help_rc != 0 )); then
|
|
595
|
+
echo "error: could not read the agy capability list — 'agy --help' exited ${aw_help_rc}. A failed probe is" >&2
|
|
596
|
+
echo " NEVER read as 'capability present', so this review refuses BEFORE any run is spent." >&2
|
|
597
|
+
echo " Installed agy version: $(aw_agy_version). Repair the install, then re-run; to update: agy update" >&2
|
|
598
|
+
exit 127
|
|
599
|
+
fi
|
|
600
|
+
# The DECLARED option tokens of the probed help, never a substring search: `--output-formatting`, or
|
|
601
|
+
# the flag merely NAMED in prose, would both open the door to a build that cannot honour the flag.
|
|
602
|
+
# A line contributes only when its first non-space character is a dash, and only its DECLARATION
|
|
603
|
+
# SEGMENT counts — the part before the description column, which help renderers separate by a run of
|
|
604
|
+
# two or more spaces or a tab. Inside that segment EVERY dash-token declares (so `-o FORMAT,
|
|
605
|
+
# --output-format FORMAT` declares both and a metavar declares nothing), and `--flag=<value>`
|
|
606
|
+
# declares `--flag`.
|
|
607
|
+
# Stated residual: a renderer that separates its columns by a SINGLE space has no machine-readable
|
|
608
|
+
# boundary at all, and a description beginning with a flag name would read as a declaration there.
|
|
609
|
+
# No such rendering is known; the guarantee below is the column convention, not a parse of English.
|
|
610
|
+
aw_declared_flags="$(printf '%s\n' "$aw_help_probe" | LC_ALL=C awk '
|
|
611
|
+
{
|
|
612
|
+
line = $0
|
|
613
|
+
sub(/^[ \t]+/, "", line)
|
|
614
|
+
if (substr(line, 1, 1) != "-") next
|
|
615
|
+
if (match(line, / +|\t/)) line = substr(line, 1, RSTART - 1)
|
|
616
|
+
n = split(line, token, /[ \t,]+/)
|
|
617
|
+
for (i = 1; i <= n; i++) {
|
|
618
|
+
t = token[i]
|
|
619
|
+
sub(/=.*$/, "", t)
|
|
620
|
+
if (substr(t, 1, 1) == "-") print t
|
|
621
|
+
}
|
|
622
|
+
}')"
|
|
623
|
+
# What each required capability BUYS — the refusal names the cost of the flag that is actually
|
|
624
|
+
# missing. One blanket explanation would be false for half of them: a build without
|
|
625
|
+
# --disable-slash-commands answers perfectly readably and corrupts the delivered BODY instead, and an
|
|
626
|
+
# operator sent hunting an unreadable envelope would be hunting the wrong bug.
|
|
627
|
+
# The subject is a NAMED local, never "$1": the source-level reverse guard reads every `case "$1"`
|
|
628
|
+
# as an argument-parser arm, and this table is not one.
|
|
629
|
+
aw_agy_flag_cost() { # $1 = required flag
|
|
630
|
+
local flag="$1"
|
|
631
|
+
case "$flag" in
|
|
632
|
+
--output-format) printf '%s' 'the answer arrives as ONE JSON envelope this wrapper reads; without it a spent turn answers in a shape the wrapper cannot read' ;;
|
|
633
|
+
--disable-slash-commands) printf '%s' 'a change-set line beginning with a slash command stays BODY; without it the CLI expands it and the model reviews something other than the delivered bytes' ;;
|
|
634
|
+
*) printf '%s' 'this dispatch passes the flag and the installed build does not declare it' ;;
|
|
635
|
+
esac
|
|
636
|
+
}
|
|
637
|
+
aw_missing_flags=()
|
|
638
|
+
for _flag in "${AW_REQUIRED_AGY_FLAGS[@]}"; do
|
|
639
|
+
case $'\n'"$aw_declared_flags"$'\n' in
|
|
640
|
+
*$'\n'"$_flag"$'\n'*) ;;
|
|
641
|
+
*) aw_missing_flags+=("$_flag") ;;
|
|
642
|
+
esac
|
|
643
|
+
done
|
|
644
|
+
if (( ${#aw_missing_flags[@]} > 0 )); then
|
|
645
|
+
echo "error: the installed agy CLI does not advertise the flag(s) this review dispatch passes: ${aw_missing_flags[*]}" >&2
|
|
646
|
+
echo " Refusing BEFORE any run is spent — what each missing capability costs:" >&2
|
|
647
|
+
for _flag in "${aw_missing_flags[@]}"; do
|
|
648
|
+
echo " ${_flag} — $(aw_agy_flag_cost "$_flag")" >&2
|
|
649
|
+
done
|
|
650
|
+
echo " Installed agy version: $(aw_agy_version). Update the CLI: agy update" >&2
|
|
651
|
+
exit 127
|
|
652
|
+
fi
|
|
653
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
654
|
+
echo "error: 'node' is not on PATH, and this review parses agy's JSON envelope in node (Node >= ${AW_MIN_NODE_MAJOR})." >&2
|
|
655
|
+
echo " Refusing BEFORE any run is spent. Install Node >= ${AW_MIN_NODE_MAJOR}, then re-run." >&2
|
|
656
|
+
exit 127
|
|
657
|
+
fi
|
|
658
|
+
aw_node_version="$(node --version 2>/dev/null || true)"
|
|
659
|
+
aw_node_major="${aw_node_version#v}"
|
|
660
|
+
aw_node_major="${aw_node_major%%.*}"
|
|
661
|
+
if [[ ! "$aw_node_major" =~ ^[0-9]+$ ]] || (( aw_node_major < AW_MIN_NODE_MAJOR )); then
|
|
662
|
+
echo "error: node '${aw_node_version:-<unreadable>}' is below the family floor (Node >= ${AW_MIN_NODE_MAJOR}) this review needs" >&2
|
|
663
|
+
echo " to parse agy's JSON envelope. Refusing BEFORE any run is spent." >&2
|
|
664
|
+
echo " Upgrade Node to >= ${AW_MIN_NODE_MAJOR}, then re-run." >&2
|
|
665
|
+
exit 127
|
|
666
|
+
fi
|
|
667
|
+
if [[ ! -f "$AGY_ENVELOPE_READER" ]]; then
|
|
668
|
+
echo "error: the envelope reader '$AGY_ENVELOPE_READER' is missing — every review reads agy's JSON" >&2
|
|
669
|
+
echo " envelope through it, so a bridge install without it could only fail AFTER a paid run." >&2
|
|
670
|
+
echo " Refusing BEFORE any run is spent. Refresh the placed bridges:" >&2
|
|
671
|
+
echo " /agent-workflow-kit setup --refresh-placed" >&2
|
|
672
|
+
exit 127
|
|
673
|
+
fi
|
|
674
|
+
|
|
502
675
|
# --- Model policy (advisory, NOT a gate) -------------------------------------
|
|
503
676
|
is_frontier=0
|
|
504
677
|
for _m in "${FRONTIER_SET[@]}"; do
|
|
@@ -1026,20 +1199,6 @@ fixed_occurrences() {
|
|
|
1026
1199
|
printf '%s' "$(( n ))"
|
|
1027
1200
|
}
|
|
1028
1201
|
|
|
1029
|
-
# The conversation id agy writes into its own run log (D9, Arm A). The format is agy's own to change,
|
|
1030
|
-
# which is exactly why an unparseable log DEGRADES LOUDLY to --continue instead of failing: the
|
|
1031
|
-
# correctness guarantee is the D1 echo proof, which fails closed if the wrong conversation answers.
|
|
1032
|
-
capture_conversation_id() { # $1 = run log → the id, or empty
|
|
1033
|
-
local id
|
|
1034
|
-
[[ -f "$1" ]] || { printf ''; return 0; }
|
|
1035
|
-
id="$(LC_ALL=C awk '/onversation/ && match($0, /[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+/) { print substr($0, RSTART, RLENGTH); exit }' "$1")"
|
|
1036
|
-
if [[ "$id" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
|
|
1037
|
-
printf '%s' "$id"
|
|
1038
|
-
else
|
|
1039
|
-
printf ''
|
|
1040
|
-
fi
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
1202
|
# A validated duration string → integer seconds (floor 1). The fed lane needs arithmetic on the cap
|
|
1044
1203
|
# because ONE wall-clock budget has to cover N+1 turns; the banner keeps printing the duration
|
|
1045
1204
|
# verbatim, so this conversion never changes what the user is told about the cap itself.
|
|
@@ -1186,7 +1345,8 @@ verify_delivery_proof() { # $1 = captured final-turn output
|
|
|
1186
1345
|
# produced (an OK, or a premature verdict) reaches this wrapper's stdout or the parsed capture, and
|
|
1187
1346
|
# the FIRST non-zero feed turn stops the run so no later turn is spent.
|
|
1188
1347
|
run_fed_review() {
|
|
1189
|
-
local k conv_id=""
|
|
1348
|
+
local k conv_id="" conv_id_file="$staging/turn1-conversation-id" feed_out="$staging/feed-turn-out" turn_rc turn_pass=()
|
|
1349
|
+
local final_envelope="$staging/final-envelope"
|
|
1190
1350
|
# ONE wall-clock budget for the WHOLE fed review, not one per turn. Handing every call the full
|
|
1191
1351
|
# AGY_HARD_TIMEOUT multiplied the stated cap by the turn count — a 30m cap silently became up to
|
|
1192
1352
|
# 30m × (N+1). Each turn now gets only what is LEFT of the shared deadline, and a review that runs
|
|
@@ -1214,9 +1374,8 @@ run_fed_review() {
|
|
|
1214
1374
|
budget=$(( remaining - AGY_TURN_KILL_GRACE_S ))
|
|
1215
1375
|
turn_hard="${budget}s"
|
|
1216
1376
|
turn_soft="$(( soft_budget < budget ? soft_budget : budget ))s"
|
|
1217
|
-
if (( k == 1 )); then turn_pass=(
|
|
1218
|
-
|
|
1219
|
-
else turn_pass=(--continue)
|
|
1377
|
+
if (( k == 1 )); then turn_pass=()
|
|
1378
|
+
else turn_pass=(--conversation "$conv_id")
|
|
1220
1379
|
fi
|
|
1221
1380
|
set +e
|
|
1222
1381
|
# Dispatched from the STAGING dir, never the work tree: agy surfaces the cwd's context file
|
|
@@ -1225,7 +1384,7 @@ run_fed_review() {
|
|
|
1225
1384
|
# byte the model can read, so the fed lane removes the uncontrolled input instead of guessing
|
|
1226
1385
|
# at it. Nothing is lost: the grounded facts already carry what the review must know.
|
|
1227
1386
|
( cd "$staging" && AGY_MODEL="$AGY_MODEL" AGY_TIMEOUT="$turn_soft" AGY_HARD_TIMEOUT="$turn_hard" \
|
|
1228
|
-
"$AGY_RUN" "@$staging/turn-$k" -- "${turn_pass[@]}" ) > "$feed_out"
|
|
1387
|
+
"$AGY_RUN" "@$staging/turn-$k" -- "${AGY_TRANSPORT_FLAGS[@]}" "${turn_pass[@]}" ) > "$feed_out"
|
|
1229
1388
|
turn_rc=$?
|
|
1230
1389
|
set -e
|
|
1231
1390
|
if (( turn_rc != 0 )); then
|
|
@@ -1233,16 +1392,34 @@ run_fed_review() {
|
|
|
1233
1392
|
echo " delivered, so no later turn is spent and NO receipt is written. Re-run the review." >&2
|
|
1234
1393
|
return "$turn_rc"
|
|
1235
1394
|
fi
|
|
1395
|
+
# Invariant E: a feed turn's envelope is validated PRIVATELY. Its response — an OK, or the
|
|
1396
|
+
# premature verdict a misbehaving turn might emit — reaches neither stdout nor the parsed
|
|
1397
|
+
# capture; only its READABILITY is consumed here. A turn that exited 0 without answering in the
|
|
1398
|
+
# shape this wrapper reads stops the run before any later turn is spent.
|
|
1399
|
+
# Turn 1 ALSO takes the conversation id, from the envelope's NAMED field, validated against the
|
|
1400
|
+
# UUID grammar by the reader. Every later turn is then routed at THIS conversation — there is no
|
|
1401
|
+
# --continue lane left to guess with, so an id that is absent, wrong-typed or malformed is the
|
|
1402
|
+
# same class of loud stop as an unreadable envelope, before turn 2 is spent.
|
|
1403
|
+
set +e
|
|
1236
1404
|
if (( k == 1 )); then
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
echo " --continue instead. Stated, never silent; the delivery proof still fails closed if" >&2
|
|
1241
|
-
echo " another conversation answers." >&2
|
|
1242
|
-
fi
|
|
1405
|
+
read_agy_envelope "$feed_out" "$staging/feed-turn-response" "$conv_id_file"
|
|
1406
|
+
else
|
|
1407
|
+
read_agy_envelope "$feed_out" "$staging/feed-turn-response"
|
|
1243
1408
|
fi
|
|
1409
|
+
turn_rc=$?
|
|
1410
|
+
set -e
|
|
1411
|
+
if (( turn_rc != 0 )); then
|
|
1412
|
+
echo "error: feed turn ${k} of ${FED_PART_COUNT} exited 0 but its answer is not a usable agy JSON" >&2
|
|
1413
|
+
echo " envelope — unreadable, or (turn 1) carrying no routable conversation id; cause above." >&2
|
|
1414
|
+
echo " Delivery cannot be confirmed, so no later turn is spent and NO receipt is written." >&2
|
|
1415
|
+
echo " Re-run the review." >&2
|
|
1416
|
+
return "$AGY_ENVELOPE_EXIT"
|
|
1417
|
+
fi
|
|
1418
|
+
if (( k == 1 )); then conv_id="$(cat "$conv_id_file")"; fi
|
|
1244
1419
|
done
|
|
1245
|
-
|
|
1420
|
+
# The partitioner refuses a zero-part change set, so this loop ran at least once: either turn 1 set
|
|
1421
|
+
# a grammar-valid conv_id or the run already returned above.
|
|
1422
|
+
turn_pass=(--conversation "$conv_id")
|
|
1246
1423
|
remaining=$(( deadline - $(date +%s) ))
|
|
1247
1424
|
if (( remaining <= AGY_TURN_KILL_GRACE_S )); then
|
|
1248
1425
|
echo "error: the fed review exhausted its hard wall-clock cap (AGY_HARD_TIMEOUT=${AGY_HARD_TIMEOUT}) before the" >&2
|
|
@@ -1256,8 +1433,20 @@ run_fed_review() {
|
|
|
1256
1433
|
turn_soft="$(( soft_budget < budget ? soft_budget : budget ))s"
|
|
1257
1434
|
set +e
|
|
1258
1435
|
( cd "$staging" && AGY_MODEL="$AGY_MODEL" AGY_TIMEOUT="$turn_soft" AGY_HARD_TIMEOUT="$turn_hard" \
|
|
1259
|
-
"$AGY_RUN" "@$staging/turn-final" -- "${turn_pass[@]}" )
|
|
1260
|
-
turn_rc
|
|
1436
|
+
"$AGY_RUN" "@$staging/turn-final" -- "${AGY_TRANSPORT_FLAGS[@]}" "${turn_pass[@]}" ) > "$final_envelope"
|
|
1437
|
+
turn_rc=$?
|
|
1438
|
+
if (( turn_rc == 0 )); then
|
|
1439
|
+
if ! read_agy_envelope "$final_envelope" "$review_out_file"; then
|
|
1440
|
+
set -e
|
|
1441
|
+
echo "error: the final review turn exited 0 but its answer is not a readable agy JSON envelope" >&2
|
|
1442
|
+
echo " (cause above). The review is UNREADABLE, not merely verdict-less: NO receipt was" >&2
|
|
1443
|
+
echo " written. Re-run the review." >&2
|
|
1444
|
+
return "$AGY_ENVELOPE_EXIT"
|
|
1445
|
+
fi
|
|
1446
|
+
cat "$review_out_file"
|
|
1447
|
+
else
|
|
1448
|
+
cat "$final_envelope"
|
|
1449
|
+
fi
|
|
1261
1450
|
set -e
|
|
1262
1451
|
return "$turn_rc"
|
|
1263
1452
|
}
|
|
@@ -1807,21 +1996,36 @@ aw_timeout_banner="$(aw_timeout_label "$aw_review_timeout_bin" "$AGY_HARD_TIMEOU
|
|
|
1807
1996
|
echo "review posture: model=${AGY_MODEL:-<agy settings default>} timeout=$aw_timeout_banner" >&2
|
|
1808
1997
|
|
|
1809
1998
|
# --- Execute via agy-run (single home of timeout + subscription + byte ceiling) ---
|
|
1810
|
-
# The
|
|
1811
|
-
#
|
|
1999
|
+
# The dispatch is captured PRIVATELY as an envelope and its `response` becomes both the
|
|
2000
|
+
# operator-visible stream and the parsed capture — one source, so what the reader sees and what the
|
|
2001
|
+
# receipt attests can never diverge.
|
|
2002
|
+
# Error priority (Decision 4): the CLI's OWN failure wins. The envelope is parsed only on a ZERO
|
|
2003
|
+
# exit; on a non-zero one the captured stdout is republished untouched and the CLI's exit code and
|
|
2004
|
+
# its stderr survive, with no envelope-parse error layered over them.
|
|
1812
2005
|
review_out_file="$staging/review-output"
|
|
2006
|
+
envelope_file="$staging/review-envelope"
|
|
2007
|
+
dispatch_flags=("${AGY_TRANSPORT_FLAGS[@]}")
|
|
2008
|
+
if (( ${#run_passthrough[@]} > 0 )); then dispatch_flags+=("${run_passthrough[@]}"); fi
|
|
1813
2009
|
set +e
|
|
1814
2010
|
if (( FED_MODE == 1 )); then
|
|
1815
2011
|
run_fed_review
|
|
1816
2012
|
rc=$?
|
|
1817
|
-
elif (( ${#run_passthrough[@]} > 0 )); then
|
|
1818
|
-
AGY_MODEL="$AGY_MODEL" AGY_TIMEOUT="$AGY_TIMEOUT" AGY_HARD_TIMEOUT="$AGY_HARD_TIMEOUT" \
|
|
1819
|
-
"$AGY_RUN" "@$prompt_file" -- "${run_passthrough[@]}" | tee "$review_out_file"
|
|
1820
|
-
rc=${PIPESTATUS[0]}
|
|
1821
2013
|
else
|
|
1822
2014
|
AGY_MODEL="$AGY_MODEL" AGY_TIMEOUT="$AGY_TIMEOUT" AGY_HARD_TIMEOUT="$AGY_HARD_TIMEOUT" \
|
|
1823
|
-
"$AGY_RUN" "@$prompt_file"
|
|
1824
|
-
rc
|
|
2015
|
+
"$AGY_RUN" "@$prompt_file" -- "${dispatch_flags[@]}" > "$envelope_file"
|
|
2016
|
+
rc=$?
|
|
2017
|
+
if (( rc == 0 )); then
|
|
2018
|
+
if ! read_agy_envelope "$envelope_file" "$review_out_file"; then
|
|
2019
|
+
set -e
|
|
2020
|
+
echo "error: the dispatch exited 0 but its answer is not a readable agy JSON envelope (cause above)." >&2
|
|
2021
|
+
echo " The review is UNREADABLE, not merely verdict-less: NO receipt was written. Re-run the" >&2
|
|
2022
|
+
echo " review; if it recurs, the installed CLI is not honouring --output-format json." >&2
|
|
2023
|
+
exit "$AGY_ENVELOPE_EXIT"
|
|
2024
|
+
fi
|
|
2025
|
+
cat "$review_out_file"
|
|
2026
|
+
else
|
|
2027
|
+
cat "$envelope_file"
|
|
2028
|
+
fi
|
|
1825
2029
|
fi
|
|
1826
2030
|
set -e
|
|
1827
2031
|
|