@sabaiway/agent-workflow-kit 1.34.0 → 1.35.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 +38 -0
- package/README.md +1 -0
- package/SKILL.md +6 -2
- package/bin/install.mjs +15 -0
- package/bridges/antigravity-cli-bridge/SKILL.md +13 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +96 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +204 -1
- package/bridges/antigravity-cli-bridge/bin/agy.sh +94 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +245 -1
- package/bridges/antigravity-cli-bridge/capability.json +17 -1
- package/bridges/codex-cli-bridge/SKILL.md +15 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +113 -0
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +288 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +114 -1
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +229 -1
- package/bridges/codex-cli-bridge/capability.json +29 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bridge-settings.md +29 -0
- package/references/modes/upgrade.md +8 -2
- package/tools/atomic-write.mjs +59 -21
- package/tools/bridge-settings-read.mjs +221 -0
- package/tools/bridge-settings.mjs +288 -0
- package/tools/commands.mjs +7 -0
- package/tools/family-registry.mjs +12 -1
- package/tools/manifest/schema.md +31 -0
- package/tools/manifest/validate.mjs +85 -0
- package/tools/procedures.mjs +26 -4
- package/tools/recipes.mjs +16 -6
- package/tools/renderers.mjs +7 -0
- package/tools/setup-backends.mjs +107 -9
- package/tools/view-model.mjs +7 -0
|
@@ -439,6 +439,23 @@ describe('codex-exec.sh — resume entrypoint restates every invariant (3.1)', (
|
|
|
439
439
|
assert.match(r.capEnv, /^FOO_API_KEY=<unset>$/m);
|
|
440
440
|
assert.match(r.argv, /(^|\n)--ignore-user-config(\n|$)/);
|
|
441
441
|
});
|
|
442
|
+
|
|
443
|
+
it('resume keeps the FULL restated policy plus the tier when set (2.3.0)', () => {
|
|
444
|
+
const sb = makeSandbox();
|
|
445
|
+
const r = run(sb, { args: ['--resume', 'sess-1', '-'], input: 'go', env: { CODEX_SERVICE_TIER: 'priority' } });
|
|
446
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
447
|
+
assert.equal(r.status, 0, r.stderr);
|
|
448
|
+
for (const inv of RESUME_INVARIANTS) assert.match(r.argv, inv, `resume argv must include ${inv}`);
|
|
449
|
+
assert.match(r.argv, /(^|\n)service_tier=priority(\n|$)/, 'a resume must not silently drop the tier');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('resume without the tier carries no service_tier flag (2.3.0)', () => {
|
|
453
|
+
const sb = makeSandbox();
|
|
454
|
+
const r = run(sb, { args: ['--resume', 'sess-1', '-'], input: 'go' });
|
|
455
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
456
|
+
assert.equal(r.status, 0, r.stderr);
|
|
457
|
+
assert.doesNotMatch(r.argv, /service_tier/);
|
|
458
|
+
});
|
|
442
459
|
});
|
|
443
460
|
|
|
444
461
|
describe('codex-exec.sh — enforced git-write boundary shim (3.2)', () => {
|
|
@@ -750,3 +767,274 @@ describe('codex-exec.sh — source-level reverse guard (parser arms ⟷ manifest
|
|
|
750
767
|
assert.ok(covered(PROBE_RELAXABLE, EXEC_CONTRACT.passthrough.probeRelaxed), 'every probe pattern has a behavioural sample');
|
|
751
768
|
});
|
|
752
769
|
});
|
|
770
|
+
|
|
771
|
+
// ── bridge settings file + service tier knob (bridges 2.3.0) ─────────────────────
|
|
772
|
+
// ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE
|
|
773
|
+
// lines, PARSED (never sourced). Precedence: explicit env (even empty: KEY= disables the
|
|
774
|
+
// knob) > file > built-in default. run() sets HOME to the sandbox repo, so the default
|
|
775
|
+
// settings path is hermetic per test.
|
|
776
|
+
|
|
777
|
+
const writeSettings = (sb, text) => {
|
|
778
|
+
const dir = join(sb.repo, '.config', 'agent-workflow');
|
|
779
|
+
mkdirSync(dir, { recursive: true });
|
|
780
|
+
const file = join(dir, 'bridge-settings.conf');
|
|
781
|
+
writeFileSync(file, text);
|
|
782
|
+
return file;
|
|
783
|
+
};
|
|
784
|
+
// chmod-based unreadability is void for root (root reads anything) — skip there.
|
|
785
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
786
|
+
|
|
787
|
+
describe('codex-exec.sh — service tier knob (bridges 2.3.0)', () => {
|
|
788
|
+
it('default: no env, no file → NO service_tier flag in codex argv', () => {
|
|
789
|
+
const sb = makeSandbox();
|
|
790
|
+
const r = run(sb);
|
|
791
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
792
|
+
assert.equal(r.status, 0, r.stderr);
|
|
793
|
+
assert.doesNotMatch(r.argv, /service_tier/, 'default OFF: the flag must be absent');
|
|
794
|
+
assert.doesNotMatch(r.stderr, /bridge settings/, 'no file → no settings chatter');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('env CODEX_SERVICE_TIER=priority → -c service_tier=priority reaches codex argv', () => {
|
|
798
|
+
const sb = makeSandbox();
|
|
799
|
+
const r = run(sb, { env: { CODEX_SERVICE_TIER: 'priority' } });
|
|
800
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
801
|
+
assert.equal(r.status, 0, r.stderr);
|
|
802
|
+
assert.match(r.argv, /(^|\n)service_tier=priority(\n|$)/);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('a file-set tier lands (file wins over the built-in default)', () => {
|
|
806
|
+
const sb = makeSandbox();
|
|
807
|
+
writeSettings(sb, 'CODEX_SERVICE_TIER=priority\n');
|
|
808
|
+
const r = run(sb);
|
|
809
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
810
|
+
assert.equal(r.status, 0, r.stderr);
|
|
811
|
+
assert.match(r.argv, /(^|\n)service_tier=priority(\n|$)/);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('an EXPLICITLY EMPTY env (CODEX_SERVICE_TIER=) disables a file-set tier for one run', () => {
|
|
815
|
+
const sb = makeSandbox();
|
|
816
|
+
writeSettings(sb, 'CODEX_SERVICE_TIER=priority\n');
|
|
817
|
+
const r = run(sb, { env: { CODEX_SERVICE_TIER: '' } });
|
|
818
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
819
|
+
assert.equal(r.status, 0, r.stderr);
|
|
820
|
+
assert.doesNotMatch(r.argv, /service_tier/, 'env wins over file — empty means knob off');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('an invalid env tier warns and runs on the standard tier (never passed to codex)', () => {
|
|
824
|
+
const sb = makeSandbox();
|
|
825
|
+
const r = run(sb, { env: { CODEX_SERVICE_TIER: 'turbo' } });
|
|
826
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
827
|
+
assert.equal(r.status, 0, r.stderr);
|
|
828
|
+
assert.match(r.stderr, /not a supported service tier/);
|
|
829
|
+
assert.doesNotMatch(r.argv, /service_tier/, 'an unvalidated value must never reach codex');
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('an invalid file tier warns and falls back to the built-in default', () => {
|
|
833
|
+
const sb = makeSandbox();
|
|
834
|
+
writeSettings(sb, 'CODEX_SERVICE_TIER=turbo\n');
|
|
835
|
+
const r = run(sb);
|
|
836
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
837
|
+
assert.equal(r.status, 0, r.stderr);
|
|
838
|
+
assert.match(r.stderr, /invalid value 'turbo'/);
|
|
839
|
+
assert.doesNotMatch(r.argv, /service_tier/);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
describe('codex-exec.sh — bridge settings file semantics (bridges 2.3.0)', () => {
|
|
845
|
+
it('env overrides file: CODEX_HARD_TIMEOUT env=2 file=9999 → killed at the env cap', () => {
|
|
846
|
+
const sb = makeSandbox();
|
|
847
|
+
writeSettings(sb, 'CODEX_HARD_TIMEOUT=9999\n');
|
|
848
|
+
const r = run(sb, { env: { CODEX_FAKE_SLEEP: '5', CODEX_HARD_TIMEOUT: '2' } });
|
|
849
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
850
|
+
assert.notEqual(r.status, 0, 'the env cap (2s) must win over the file cap (9999s)');
|
|
851
|
+
assert.match(r.stderr, /exceeded the hard cap CODEX_HARD_TIMEOUT=2s/);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('a file-set CODEX_HARD_TIMEOUT is effective (killed at the file cap)', () => {
|
|
855
|
+
const sb = makeSandbox();
|
|
856
|
+
writeSettings(sb, 'CODEX_HARD_TIMEOUT=2\n');
|
|
857
|
+
const r = run(sb, { env: { CODEX_FAKE_SLEEP: '5' } });
|
|
858
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
859
|
+
assert.notEqual(r.status, 0, 'the file cap must apply when the env is unset');
|
|
860
|
+
assert.match(r.stderr, /exceeded the hard cap CODEX_HARD_TIMEOUT=2s/);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it("another wrapper's / another bridge's valid key is skipped silently", () => {
|
|
864
|
+
const sb = makeSandbox();
|
|
865
|
+
writeSettings(sb, 'CODEX_REVIEW_MAX_TOTAL_BYTES=100\nAGY_HARD_TIMEOUT=30m\nAGY_REVIEW_ALLOW_ADDDIR=1\n');
|
|
866
|
+
const r = run(sb);
|
|
867
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
868
|
+
assert.equal(r.status, 0, r.stderr);
|
|
869
|
+
assert.doesNotMatch(r.stderr, /bridge settings/, 'a recognized non-applied key earns NO warning');
|
|
870
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it('a truly unknown key warns ONCE naming the file; the run is unaffected', () => {
|
|
874
|
+
const sb = makeSandbox();
|
|
875
|
+
writeSettings(sb, 'TOTALLY_UNKNOWN=1\nTOTALLY_UNKNOWN=2\n');
|
|
876
|
+
const r = run(sb);
|
|
877
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
878
|
+
assert.equal(r.status, 0, r.stderr);
|
|
879
|
+
const warns = r.stderr.match(/unknown key 'TOTALLY_UNKNOWN'/g) ?? [];
|
|
880
|
+
assert.equal(warns.length, 1, `exactly one warning per unknown key, got ${warns.length}`);
|
|
881
|
+
assert.match(r.stderr, /bridge-settings\.conf/, 'the warning must name the settings file');
|
|
882
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('duplicate key → the LAST occurrence wins (invalid then valid → applied, no warning)', () => {
|
|
886
|
+
const sb = makeSandbox();
|
|
887
|
+
writeSettings(sb, 'CODEX_SERVICE_TIER=bogus\nCODEX_SERVICE_TIER=priority\n');
|
|
888
|
+
const r = run(sb);
|
|
889
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
890
|
+
assert.equal(r.status, 0, r.stderr);
|
|
891
|
+
assert.match(r.argv, /(^|\n)service_tier=priority(\n|$)/);
|
|
892
|
+
assert.doesNotMatch(r.stderr, /invalid value/, 'only the LAST occurrence is the value');
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('duplicate key → the LAST occurrence wins (valid then invalid → warned + default)', () => {
|
|
896
|
+
const sb = makeSandbox();
|
|
897
|
+
writeSettings(sb, 'CODEX_SERVICE_TIER=priority\nCODEX_SERVICE_TIER=bogus\n');
|
|
898
|
+
const r = run(sb);
|
|
899
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
900
|
+
assert.equal(r.status, 0, r.stderr);
|
|
901
|
+
assert.match(r.stderr, /invalid value 'bogus'/);
|
|
902
|
+
assert.doesNotMatch(r.argv, /service_tier/);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('malformed lines warn and are ignored; comments and blank lines are silent', () => {
|
|
906
|
+
const sb = makeSandbox();
|
|
907
|
+
writeSettings(sb, '# a comment\n\nNOT A KEY VALUE LINE\nCODEX_SERVICE_TIER=priority\n');
|
|
908
|
+
const r = run(sb);
|
|
909
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
910
|
+
assert.equal(r.status, 0, r.stderr);
|
|
911
|
+
assert.match(r.stderr, /malformed line/);
|
|
912
|
+
const malformed = r.stderr.match(/malformed line/g) ?? [];
|
|
913
|
+
assert.equal(malformed.length, 1, 'comments/blank lines must NOT count as malformed');
|
|
914
|
+
assert.match(r.argv, /(^|\n)service_tier=priority(\n|$)/, 'valid lines still apply');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('an existing-but-unreadable file warns loudly and falls back to built-ins', { skip: isRoot }, () => {
|
|
918
|
+
const sb = makeSandbox();
|
|
919
|
+
const file = writeSettings(sb, 'CODEX_SERVICE_TIER=priority\n');
|
|
920
|
+
chmodSync(file, 0o000);
|
|
921
|
+
const r = run(sb);
|
|
922
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
923
|
+
assert.equal(r.status, 0, r.stderr);
|
|
924
|
+
assert.match(r.stderr, /unreadable/);
|
|
925
|
+
assert.doesNotMatch(r.argv, /service_tier/, 'an unreadable file must yield built-in defaults');
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('a settings line can NEVER execute code (command-substitution payload inert)', () => {
|
|
929
|
+
const sb = makeSandbox();
|
|
930
|
+
const pwned = join(sb.repo, 'pwned');
|
|
931
|
+
const pwned2 = join(sb.repo, 'pwned2');
|
|
932
|
+
writeSettings(
|
|
933
|
+
sb,
|
|
934
|
+
`CODEX_SERVICE_TIER=$(touch ${pwned})\nEVIL_KEY=\`touch ${pwned2}\`\n`,
|
|
935
|
+
);
|
|
936
|
+
const r = run(sb);
|
|
937
|
+
const executed = existsSync(pwned) || existsSync(pwned2);
|
|
938
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
939
|
+
assert.equal(r.status, 0, r.stderr);
|
|
940
|
+
assert.equal(executed, false, 'file content must be parsed, never evaluated');
|
|
941
|
+
assert.doesNotMatch(r.argv, /service_tier/, 'the payload value must fail validation');
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('a DIRECTORY at the settings path warns loudly and falls back to built-ins (no crash)', () => {
|
|
945
|
+
const sb = makeSandbox();
|
|
946
|
+
mkdirSync(join(sb.repo, '.config', 'agent-workflow', 'bridge-settings.conf'), { recursive: true });
|
|
947
|
+
const r = run(sb);
|
|
948
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
949
|
+
assert.equal(r.status, 0, `a directory must degrade honestly, not kill the run: ${r.stderr}`);
|
|
950
|
+
assert.match(r.stderr, /unreadable or not a regular file/);
|
|
951
|
+
assert.doesNotMatch(r.stderr, /Is a directory/, 'no raw bash error may leak');
|
|
952
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'the run must proceed on built-ins');
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it('a FIFO at the settings path warns and falls back (never opened — no pre-timeout hang)', () => {
|
|
956
|
+
const sb = makeSandbox();
|
|
957
|
+
const dir = join(sb.repo, '.config', 'agent-workflow');
|
|
958
|
+
mkdirSync(dir, { recursive: true });
|
|
959
|
+
const fifo = join(dir, 'bridge-settings.conf');
|
|
960
|
+
const mk = spawnSync('mkfifo', [fifo]);
|
|
961
|
+
if (mk.status !== 0) { rmSync(sb.root, { recursive: true, force: true }); return; } // no mkfifo here — the directory case covers the class
|
|
962
|
+
const r = run(sb);
|
|
963
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
964
|
+
assert.equal(r.status, 0, r.stderr);
|
|
965
|
+
assert.match(r.stderr, /unreadable or not a regular file/);
|
|
966
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it('XDG_CONFIG_HOME relocates the settings file', () => {
|
|
970
|
+
const sb = makeSandbox();
|
|
971
|
+
const xdg = join(sb.root, 'xdg');
|
|
972
|
+
mkdirSync(join(xdg, 'agent-workflow'), { recursive: true });
|
|
973
|
+
writeFileSync(join(xdg, 'agent-workflow', 'bridge-settings.conf'), 'CODEX_SERVICE_TIER=priority\n');
|
|
974
|
+
const r = run(sb, { env: { XDG_CONFIG_HOME: xdg } });
|
|
975
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
976
|
+
assert.equal(r.status, 0, r.stderr);
|
|
977
|
+
assert.match(r.argv, /(^|\n)service_tier=priority(\n|$)/);
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// ── settings surface ⟷ manifest (drift guard, D6) ────────────────────────────────
|
|
982
|
+
// The manifest `settings` block is the single source: the --help Settings section
|
|
983
|
+
// renders this wrapper's applied subset, and the shell constants (registry, applied
|
|
984
|
+
// subset, typed validation arms) stay set-equal to the UNION of both bridges' blocks
|
|
985
|
+
// (the reader recognizes every family key). The sibling manifest resolves identically
|
|
986
|
+
// in the repo layout and in the kit's bridges/ mirror layout.
|
|
987
|
+
const SETTINGS_HEADER = 'Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):';
|
|
988
|
+
const SIBLING_MANIFEST = JSON.parse(readFileSync(join(HERE, '..', '..', 'antigravity-cli-bridge', 'capability.json'), 'utf8'));
|
|
989
|
+
const ALL_SETTINGS = [...(MANIFEST.settings ?? []), ...(SIBLING_MANIFEST.settings ?? [])];
|
|
990
|
+
const SETTINGS_CMD = 'codex-exec';
|
|
991
|
+
|
|
992
|
+
describe('codex-exec.sh — settings surface ⟷ manifest (D6, manifest-pinned)', () => {
|
|
993
|
+
it('--help Settings section keys set-EQUAL the manifest appliesTo subset', () => {
|
|
994
|
+
const help = runHelp('--help').stdout;
|
|
995
|
+
const section = helpSection(help, SETTINGS_HEADER);
|
|
996
|
+
const got = section.filter((l) => /^[A-Z][A-Z0-9_]+ —/.test(l)).map((l) => l.split(' ')[0]);
|
|
997
|
+
const want = (MANIFEST.settings ?? []).filter((s) => s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
|
|
998
|
+
assert.ok(want.length > 0, 'the manifest must declare settings for this wrapper');
|
|
999
|
+
setEq(got, want, 'help Settings keys ⟷ manifest settings.appliesTo');
|
|
1000
|
+
assert.ok(section.some((l) => l.includes('agent-workflow/bridge-settings.conf')), 'the section names the settings file');
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
const source = readFileSync(WRAPPER, 'utf8');
|
|
1004
|
+
|
|
1005
|
+
it('aw_settings_known carries exactly the UNION of both bridges settings keys', () => {
|
|
1006
|
+
const m = source.match(/aw_settings_known\(\) \{\n case " ([^"]+) " in/);
|
|
1007
|
+
assert.ok(m, 'aw_settings_known registry case not found');
|
|
1008
|
+
assert.ok(ALL_SETTINGS.length >= 5, 'both manifests must contribute settings');
|
|
1009
|
+
setEq(m[1].trim().split(/\s+/), ALL_SETTINGS.map((s) => s.key), 'shell registry ⟷ manifest union');
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('AW_SETTINGS_APPLIED equals the manifest appliesTo subset for this wrapper', () => {
|
|
1013
|
+
const m = source.match(/^AW_SETTINGS_APPLIED="([^"]*)"$/m);
|
|
1014
|
+
assert.ok(m, 'AW_SETTINGS_APPLIED not found');
|
|
1015
|
+
const want = ALL_SETTINGS.filter((s) => s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
|
|
1016
|
+
assert.ok(want.length > 0);
|
|
1017
|
+
setEq(m[1].trim().split(/\s+/), want, 'applied subset ⟷ manifest appliesTo');
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('aw_settings_valid arms carry the manifest typed constants per key', () => {
|
|
1021
|
+
const body = source.match(/aw_settings_valid\(\) \{[\s\S]*?\n\}/);
|
|
1022
|
+
assert.ok(body, 'aw_settings_valid not found');
|
|
1023
|
+
const armKeys = [...body[0].matchAll(/^ ([A-Z][A-Z0-9_]*)\)/gm)].map((x) => x[1]);
|
|
1024
|
+
setEq(armKeys, ALL_SETTINGS.map((s) => s.key), 'validation arms ⟷ manifest keys');
|
|
1025
|
+
for (const s of ALL_SETTINGS) {
|
|
1026
|
+
const arm = body[0].match(new RegExp(`^ ${s.key}\\) (.*) ;;$`, 'm'));
|
|
1027
|
+
assert.ok(arm, `no validation arm for ${s.key}`);
|
|
1028
|
+
if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
|
|
1029
|
+
if (s.kind === 'integer') {
|
|
1030
|
+
assert.match(arm[1], new RegExp(`>= ${s.min}\\b`), `${s.key}: min ${s.min} not pinned`);
|
|
1031
|
+
assert.match(arm[1], new RegExp(`<= ${s.max}\\b`), `${s.key}: max ${s.max} not pinned`);
|
|
1032
|
+
}
|
|
1033
|
+
if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
|
|
1034
|
+
if (s.kind === 'duration') {
|
|
1035
|
+
assert.ok(arm[1].includes('$dur_re'), `${s.key}: duration grammar not pinned`);
|
|
1036
|
+
assert.ok(arm[1].includes('$zero_re'), `${s.key}: zero-duration rejection not pinned (timeout 0 disables the cap)`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
@@ -61,6 +61,12 @@ Receipt:
|
|
|
61
61
|
verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge,
|
|
62
62
|
factsHash null); a write failure warns, never fails the review
|
|
63
63
|
|
|
64
|
+
Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
|
|
65
|
+
${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
|
|
66
|
+
CODEX_SERVICE_TIER — service tier: 'priority' (Fast — ~1.5x speed at a 2.5x credit rate on gpt-5.5); a consented SPEND knob, default off (standard tier)
|
|
67
|
+
CODEX_HARD_TIMEOUT — hard wall-clock cap, integer seconds 1..86400 (built-in default 1800)
|
|
68
|
+
CODEX_REVIEW_MAX_TOTAL_BYTES — inline-payload cap, integer bytes 1..100000000 (default 1500000); above it the diff rides via a git-dir temp file
|
|
69
|
+
|
|
64
70
|
Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only), AW_REVIEW_RECEIPTS (receipt file override).
|
|
65
71
|
Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
|
|
66
72
|
HELP
|
|
@@ -68,12 +74,102 @@ HELP
|
|
|
68
74
|
;;
|
|
69
75
|
esac
|
|
70
76
|
|
|
77
|
+
# This wrapper's applied settings-file subset (see the shared reader block below).
|
|
78
|
+
AW_SETTINGS_APPLIED="CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT CODEX_REVIEW_MAX_TOTAL_BYTES"
|
|
79
|
+
|
|
80
|
+
# --- Bridge settings file (host-level, kit-independent) — byte-identical across the four wrappers ---
|
|
81
|
+
# ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE lines,
|
|
82
|
+
# PARSED (grep/case), NEVER sourced — a file line can never execute code. Precedence: explicit
|
|
83
|
+
# env (even empty: KEY= disables the knob for one run) > file > built-in default. Each wrapper
|
|
84
|
+
# APPLIES only its own subset ($AW_SETTINGS_APPLIED, set above this block) but RECOGNIZES the
|
|
85
|
+
# whole registry: a key belonging to another wrapper or another bridge is skipped silently; only
|
|
86
|
+
# a key unknown to the entire registry warns (once per key), naming this file as the source.
|
|
87
|
+
# A malformed line warns and is ignored; a value failing the key's typed validation warns and
|
|
88
|
+
# falls back to the built-in default (never passed to the binary); duplicate key → the LAST
|
|
89
|
+
# occurrence wins; a missing file is silent; an existing-but-unreadable or non-regular file
|
|
90
|
+
# warns loudly and falls back to built-in defaults (a directory or FIFO is never opened).
|
|
91
|
+
# Diagnostics are emitted once per user-visible run: a delegating wrapper (agy-review →
|
|
92
|
+
# agy-run) exports AW_SETTINGS_NOTIFIED so the child never repeats the same file's warnings.
|
|
93
|
+
# The registry, per-wrapper subsets, and typed constants mirror
|
|
94
|
+
# the bridges' capability.json `settings` blocks (manifest-as-source, drift-guarded by tests).
|
|
95
|
+
aw_settings_file() {
|
|
96
|
+
printf '%s/agent-workflow/bridge-settings.conf' "${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
97
|
+
}
|
|
98
|
+
aw_settings_known() {
|
|
99
|
+
case " CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT CODEX_REVIEW_MAX_TOTAL_BYTES AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR " in
|
|
100
|
+
*" $1 "*) return 0 ;;
|
|
101
|
+
*) return 1 ;;
|
|
102
|
+
esac
|
|
103
|
+
}
|
|
104
|
+
aw_settings_valid() {
|
|
105
|
+
local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
|
|
106
|
+
case "$k" in
|
|
107
|
+
CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
|
|
108
|
+
CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
|
|
109
|
+
CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
|
|
110
|
+
AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
|
|
111
|
+
AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
|
|
112
|
+
*) return 1 ;;
|
|
113
|
+
esac
|
|
114
|
+
}
|
|
115
|
+
aw_apply_settings() {
|
|
116
|
+
local file line key value warned notify
|
|
117
|
+
file="$(aw_settings_file)"
|
|
118
|
+
[[ -e "$file" ]] || return 0
|
|
119
|
+
notify=1
|
|
120
|
+
[[ -n "${AW_SETTINGS_NOTIFIED:-}" ]] && notify=0
|
|
121
|
+
export AW_SETTINGS_NOTIFIED=1
|
|
122
|
+
if [[ ! -f "$file" || ! -r "$file" ]]; then
|
|
123
|
+
if (( notify )); then
|
|
124
|
+
echo "warning: bridge settings file '$file' exists but is unreadable or not a regular file — using built-in defaults." >&2
|
|
125
|
+
fi
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
if (( notify )); then
|
|
129
|
+
warned=" "
|
|
130
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
131
|
+
[[ -z "${line//[[:space:]]/}" ]] && continue
|
|
132
|
+
case "${line#"${line%%[![:space:]]*}"}" in "#"*) continue ;; esac
|
|
133
|
+
if [[ ! "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
|
134
|
+
echo "warning: malformed line in bridge settings file '$file' (ignored): $line" >&2
|
|
135
|
+
continue
|
|
136
|
+
fi
|
|
137
|
+
key="${line%%=*}"
|
|
138
|
+
if ! aw_settings_known "$key"; then
|
|
139
|
+
case "$warned" in
|
|
140
|
+
*" $key "*) : ;;
|
|
141
|
+
*)
|
|
142
|
+
warned="$warned$key "
|
|
143
|
+
echo "warning: unknown key '$key' in bridge settings file '$file' (ignored)." >&2
|
|
144
|
+
;;
|
|
145
|
+
esac
|
|
146
|
+
fi
|
|
147
|
+
done <"$file"
|
|
148
|
+
fi
|
|
149
|
+
for key in $AW_SETTINGS_APPLIED; do
|
|
150
|
+
if [[ -n "${!key+x}" ]]; then continue; fi
|
|
151
|
+
value="$(grep "^${key}=" "$file" 2>/dev/null || true)"
|
|
152
|
+
[[ -n "$value" ]] || continue
|
|
153
|
+
value="${value##*$'\n'}"
|
|
154
|
+
value="${value#*=}"
|
|
155
|
+
if ! aw_settings_valid "$key" "$value"; then
|
|
156
|
+
if (( notify )); then
|
|
157
|
+
echo "warning: invalid value '$value' for $key in bridge settings file '$file' — using the built-in default." >&2
|
|
158
|
+
fi
|
|
159
|
+
continue
|
|
160
|
+
fi
|
|
161
|
+
export "$key=$value"
|
|
162
|
+
done
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
165
|
+
aw_apply_settings
|
|
166
|
+
|
|
71
167
|
DEFAULT_CODEX_MODEL="gpt-5.5"
|
|
72
168
|
DEFAULT_CODEX_EFFORT="xhigh"
|
|
73
169
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
74
170
|
# version (drift-guarded by codex-review.test.mjs against capability.json).
|
|
75
171
|
AW_RECEIPT_BACKEND="codex"
|
|
76
|
-
AW_BRIDGE_VERSION="2.
|
|
172
|
+
AW_BRIDGE_VERSION="2.3.0"
|
|
77
173
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
78
174
|
CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
|
|
79
175
|
# Generous hard cap for a slow xhigh review (subscription latency varies).
|
|
@@ -81,6 +177,22 @@ CODEX_HARD_TIMEOUT="${CODEX_HARD_TIMEOUT:-1800}"
|
|
|
81
177
|
# Above this assembled-payload size (bytes), the diff goes via a git-dir-local temp
|
|
82
178
|
# file instead of inline — never truncated.
|
|
83
179
|
CODEX_REVIEW_MAX_TOTAL_BYTES="${CODEX_REVIEW_MAX_TOTAL_BYTES:-1500000}"
|
|
180
|
+
# Codex service tier (quality-neutral speed knob; live-probed 2026-07-05): default EMPTY ⇒ no
|
|
181
|
+
# service_tier flag (standard tier) — enabling Fast is a consented per-host SPEND act, never a
|
|
182
|
+
# silent default. The only server-catalog tier id on this subscription is 'priority' (catalog
|
|
183
|
+
# display name "Fast": ~1.5x token speed at a 2.5x credit rate on gpt-5.5; quality-neutral —
|
|
184
|
+
# same model). codex itself accepts ANY -c service_tier string silently (probe-verified), so
|
|
185
|
+
# the wrapper validates the effective value: an unsupported one warns and runs on the standard
|
|
186
|
+
# tier — a typo can never silently masquerade as Fast.
|
|
187
|
+
CODEX_SERVICE_TIER="${CODEX_SERVICE_TIER:-}"
|
|
188
|
+
if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
|
|
189
|
+
echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
|
|
190
|
+
CODEX_SERVICE_TIER=""
|
|
191
|
+
fi
|
|
192
|
+
tier_flags=()
|
|
193
|
+
if [[ -n "$CODEX_SERVICE_TIER" ]]; then
|
|
194
|
+
tier_flags=(-c "service_tier=$CODEX_SERVICE_TIER")
|
|
195
|
+
fi
|
|
84
196
|
CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
|
|
85
197
|
|
|
86
198
|
# --- Quality-first guard: refuse a silent model/effort downgrade ---------------
|
|
@@ -360,6 +472,7 @@ codex_flags=(codex exec
|
|
|
360
472
|
-c model_reasoning_effort="$CODEX_EFFORT"
|
|
361
473
|
-c hide_agent_reasoning=true
|
|
362
474
|
-c model_reasoning_summary=none
|
|
475
|
+
"${tier_flags[@]+"${tier_flags[@]}"}"
|
|
363
476
|
--color never
|
|
364
477
|
-o "$out"
|
|
365
478
|
--json
|