Skip to content

Commit 38fb537

Browse files
authored
Merge pull request #205 from Fr-e-d/contrib/sync-1776718216
sync: update 4 file(s) in core/
2 parents 8927e11 + e41d51d commit 38fb537

4 files changed

Lines changed: 112 additions & 28 deletions

File tree

.gaai/core/adapters/claude-code/__tests__/impl-routing.test.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ describe('resolveImplRouting', () => {
5454
assert.ok(warnings.some(w => w.includes('GAAI_IMPL_MODEL')), 'warning must mention GAAI_IMPL_MODEL');
5555
});
5656

57-
// AC10: impl_model absent → primary, no routing warnings
58-
test('AC10: impl_model absent → provider: primary, no routing log', () => {
57+
// AC10 (DEC-72): impl_model absent + env MISSING → primary, no routing warnings
58+
// Preserves OSS non-regression (E94 D-0 intent scoped to pre-configuration state).
59+
test('AC10: impl_model absent + env missing → provider: primary, no routing log (OSS non-regression)', () => {
5960
clearEnv();
6061
const warnings = [];
6162
const orig = console.warn;
@@ -71,14 +72,60 @@ describe('resolveImplRouting', () => {
7172
assert.equal(warnings.filter(w => w.includes('IMPL_ROUTING')).length, 0, 'no IMPL_ROUTING warnings on primary path');
7273
});
7374

74-
// AC10 variant: impl_model: "primary" → same as absent
75-
test('AC10b: impl_model: primary → provider: primary', () => {
75+
// DEC-72: impl_model absent + env CONFIGURED → secondary (NEW default post-E94 validation)
76+
test('DEC-72: impl_model absent + env configured → provider: secondary (env-driven default)', () => {
77+
setValidEnv();
78+
const r = resolveImplRouting(undefined);
79+
assert.equal(r.provider, 'secondary');
80+
assert.equal(r.implModelTag, null);
81+
assert.equal(r.envMissing, null);
82+
assert.equal(r.reason, null);
83+
});
84+
85+
// DEC-72: impl_model null (explicit null) + env configured → secondary
86+
test('DEC-72: impl_model null + env configured → provider: secondary', () => {
87+
setValidEnv();
88+
const r = resolveImplRouting(null);
89+
assert.equal(r.provider, 'secondary');
90+
assert.equal(r.implModelTag, null);
91+
});
92+
93+
// DEC-72: impl_model absent + partial env → primary (no warn because no explicit opt-in)
94+
test('DEC-72: impl_model absent + partial env → provider: primary, no warn (silent fallback)', () => {
95+
process.env.GAAI_IMPL_BASE_URL = 'https://test.example.com';
96+
// AUTH_TOKEN and MODEL not set
97+
const warnings = [];
98+
const orig = console.warn;
99+
console.warn = (...args) => warnings.push(args.join(' '));
100+
101+
const r = resolveImplRouting(undefined);
102+
103+
console.warn = orig;
104+
assert.equal(r.provider, 'primary');
105+
assert.equal(r.reason, null, 'no reason when tag is absent (silent fallback, not opt-in failure)');
106+
assert.equal(r.envMissing, null, 'envMissing is null when tag is absent (no explicit opt-in to report)');
107+
assert.equal(warnings.filter(w => w.includes('IMPL_ROUTING')).length, 0, 'no warn when tag absent');
108+
});
109+
110+
// AC10b (DEC-72): impl_model: "primary" → always primary, regardless of env (explicit opt-out)
111+
test('AC10b: impl_model: primary + env missing → provider: primary (explicit opt-out)', () => {
112+
clearEnv();
76113
const r = resolveImplRouting('primary');
77114
assert.equal(r.provider, 'primary');
78115
assert.equal(r.implModelTag, 'primary');
79116
assert.equal(r.reason, null);
80117
});
81118

119+
// DEC-72: impl_model: "primary" + env configured → STILL primary (explicit opt-out is absolute)
120+
test('DEC-72: impl_model: primary + env configured → provider: primary (explicit opt-out overrides env default)', () => {
121+
setValidEnv();
122+
const r = resolveImplRouting('primary');
123+
assert.equal(r.provider, 'primary');
124+
assert.equal(r.implModelTag, 'primary');
125+
assert.equal(r.reason, null);
126+
assert.equal(r.envMissing, null);
127+
});
128+
82129
// AC7: decision is stable (called multiple times with same args → same provider)
83130
test('AC7: decision is deterministic (same input → same provider)', () => {
84131
setValidEnv();

.gaai/core/adapters/claude-code/impl-routing.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@
66
*
77
* Called once per story at Implementation phase entry. Result is stable for the
88
* entire phase — mid-phase re-evaluation is forbidden (E94S04 AC7).
9+
*
10+
* Default routing (DEC-72, 2026-04-20, amends E94 D-0):
11+
* - tag === 'primary' → primary (explicit opt-out)
12+
* - tag === 'secondary' + env → secondary
13+
* - tag === 'secondary' + !env → primary (warn, reason='secondary_but_env_missing')
14+
* - tag absent + env → secondary (NEW default — was primary under E94 D-0)
15+
* - tag absent + !env → primary (preserves OSS non-regression)
916
*/
1017

1118
import { randomUUID } from 'node:crypto';
1219

20+
const REQUIRED_ENV = ['GAAI_IMPL_BASE_URL', 'GAAI_IMPL_AUTH_TOKEN', 'GAAI_IMPL_MODEL'];
21+
1322
/**
1423
* @typedef {Object} RoutingDecision
1524
* @property {'primary'|'secondary'} provider - Which provider path to use
@@ -19,6 +28,10 @@ import { randomUUID } from 'node:crypto';
1928
* @property {string|null} reason - 'secondary_but_env_missing' | null
2029
*/
2130

31+
function checkEnv() {
32+
return REQUIRED_ENV.filter(name => !process.env[name]?.trim());
33+
}
34+
2235
/**
2336
* Resolves the Implementation phase routing decision.
2437
*
@@ -29,22 +42,27 @@ export function resolveImplRouting(implModelTag) {
2942
const traceId = randomUUID();
3043
const tag = implModelTag ?? null;
3144

32-
if (tag !== 'secondary') {
45+
// Explicit opt-out — always routes primary, regardless of env (DEC-72)
46+
if (tag === 'primary') {
3347
return { provider: 'primary', traceId, implModelTag: tag, envMissing: null, reason: null };
3448
}
3549

36-
// Pre-flight env check — never attempt secondary if any required var is missing or empty
37-
const missing = [];
38-
if (!process.env.GAAI_IMPL_BASE_URL?.trim()) missing.push('GAAI_IMPL_BASE_URL');
39-
if (!process.env.GAAI_IMPL_AUTH_TOKEN?.trim()) missing.push('GAAI_IMPL_AUTH_TOKEN');
40-
if (!process.env.GAAI_IMPL_MODEL?.trim()) missing.push('GAAI_IMPL_MODEL');
50+
// Env pre-flight gate — shared by explicit 'secondary' and env-driven default (DEC-72)
51+
const missing = checkEnv();
4152

4253
if (missing.length > 0) {
43-
console.warn(
44-
`IMPL_ROUTING_ENV_MISSING: expected GAAI_IMPL_BASE_URL|AUTH_TOKEN|MODEL, got: ${missing.join(', ')}`
45-
);
46-
return { provider: 'primary', traceId, implModelTag: tag, envMissing: missing, reason: 'secondary_but_env_missing' };
54+
// Env missing: stories without env cannot route secondary.
55+
// - tag === 'secondary' → warn + fallback (existing E94 behavior)
56+
// - tag absent → silent primary (preserves OSS non-regression — DEC-72)
57+
if (tag === 'secondary') {
58+
console.warn(
59+
`IMPL_ROUTING_ENV_MISSING: expected ${REQUIRED_ENV.join('|')}, got: ${missing.join(', ')}`
60+
);
61+
return { provider: 'primary', traceId, implModelTag: tag, envMissing: missing, reason: 'secondary_but_env_missing' };
62+
}
63+
return { provider: 'primary', traceId, implModelTag: tag, envMissing: null, reason: null };
4764
}
4865

66+
// Env configured: route secondary (tag === 'secondary' OR tag absent → new default per DEC-72)
4967
return { provider: 'secondary', traceId, implModelTag: tag, envMissing: null, reason: null };
5068
}

.gaai/core/workflows/delivery-loop.workflow.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ node .gaai/core/adapters/claude-code/runtime-routing-logger.js \
221221
The routing decision is evaluated **exactly once** at Implementation phase entry. Mid-phase changes to env vars or the backlog entry do NOT re-trigger evaluation.
222222
223223
> **🔒 Mode invariance (NON-NEGOTIABLE — applies to ALL Implementation Agents):**
224-
> The decision to invoke `nested-claude-spawn.js` is based EXCLUSIVELY on the two conditions in the decision matrix below:
225-
> (a) `impl_model == "secondary"` in the backlog entry, AND
224+
> The decision to invoke `nested-claude-spawn.js` is based EXCLUSIVELY on the conditions in the decision matrix below (per DEC-72, amends E94 D-0):
225+
> (a) `impl_model != "primary"` in the backlog entry (i.e., `secondary` OR absent), AND
226226
> (b) all three `GAAI_IMPL_*` environment variables are present and non-empty.
227227
>
228228
> **No other factor influences this decision.** In particular:
@@ -232,7 +232,7 @@ The routing decision is evaluated **exactly once** at Implementation phase entry
232232
>
233233
> **If you (the agent) find yourself reasoning "I'll use primary because X":** STOP. X is invalid unless X is literally one of the two matrix conditions. Route to nested wrapper per the matrix.
234234
>
235-
> **Rationale:** Epic E94 D-0 (non-regression) is preserved by the `impl_model` tag itself — stories without the tag use primary. Agents second-guessing the matrix defeat the entire Epic's purpose (quota savings) and produce misleading `runtime-routing.jsonl` logs (e.g., `secondary_but_env_missing` logged when env was actually present).
235+
> **Rationale:** Epic E94 D-0's OSS non-regression intent is preserved by condition (b) — users without `GAAI_IMPL_*` env vars configured see zero behavioral change. DEC-72 (2026-04-20) amends the post-E94S10 default: when env vars are configured, secondary is used by default (tag absent → secondary). `impl_model: primary` is the explicit opt-out for stories that must run on primary (complex reasoning, frontend/UX judgment, security-critical). Agents second-guessing the matrix defeat the entire Epic's purpose (quota savings) and produce misleading `runtime-routing.jsonl` logs.
236236
237237
**Decision matrix:**
238238
@@ -246,16 +246,30 @@ read impl_model from backlog entry # canonical runtime source per E94S02 AC6
246246
# - No circuit-breaker auto-disable of secondary
247247
# Fallback is atomic binary: secondary failed → primary runs (or escalates). T-2.
248248
249-
if impl_model == "secondary":
249+
# DEC-72 (2026-04-20): tag absent + env configured routes to secondary by default.
250+
# tag === 'primary' → explicit opt-out (always primary, regardless of env).
251+
252+
if impl_model == "primary":
253+
# Explicit opt-out — always primary, regardless of env
254+
node .gaai/core/adapters/claude-code/runtime-routing-logger.js \
255+
--trace-id "$STORY_TRACE_ID" --story-id "{id}" --phase "impl" \
256+
--provider "primary" --model "${CLAUDE_MODEL:-claude-sonnet-4-6}" \
257+
--duration-ms 0 --fallback-reason "" --impl-model-tag "primary" 2>/dev/null || true
258+
route → Task tool on primary
259+
260+
elif impl_model == "secondary" OR impl_model is absent:
250261
# Pre-flight env check (never attempt secondary if env missing)
251262
missing = []
252263
if not GAAI_IMPL_BASE_URL (non-empty): missing += ["GAAI_IMPL_BASE_URL"]
253264
if not GAAI_IMPL_AUTH_TOKEN(non-empty): missing += ["GAAI_IMPL_AUTH_TOKEN"]
254265
if not GAAI_IMPL_MODEL (non-empty): missing += ["GAAI_IMPL_MODEL"]
255266
256267
if missing:
257-
warn("IMPL_ROUTING_ENV_MISSING: expected GAAI_IMPL_BASE_URL|AUTH_TOKEN|MODEL, got: " + join(missing, ", "))
258-
echo "⚠ impl_model=secondary but GAAI_IMPL_* env vars missing; using primary."
268+
# Only warn when the user explicitly opted in via tag='secondary'.
269+
# When tag is absent and env is missing, silent primary fallback preserves OSS non-regression (E94 D-0 intent, DEC-72).
270+
if impl_model == "secondary":
271+
warn("IMPL_ROUTING_ENV_MISSING: expected GAAI_IMPL_BASE_URL|AUTH_TOKEN|MODEL, got: " + join(missing, ", "))
272+
echo "⚠ impl_model=secondary but GAAI_IMPL_* env vars missing; using primary."
259273
node .gaai/core/adapters/claude-code/runtime-routing-logger.js \
260274
--trace-id "$STORY_TRACE_ID" --story-id "{id}" --phase "impl" \
261275
--provider "primary" --model "${CLAUDE_MODEL:-claude-sonnet-4-6}" \
@@ -312,16 +326,13 @@ if impl_model == "secondary":
312326
ESCALATE via existing daemon behavior (backlog status update + notes)
313327
# Note: no retry, no tier-based strategy, no circuit-breaker (AC3, AC18, AC19)
314328
315-
elif impl_model == "primary" OR impl_model is absent:
316-
# Task tool on primary — byte-for-byte identical to pre-Epic delivery
317-
node .gaai/core/adapters/claude-code/runtime-routing-logger.js \
318-
--trace-id "$STORY_TRACE_ID" --story-id "{id}" --phase "impl" \
319-
--provider "primary" --model "${CLAUDE_MODEL:-claude-sonnet-4-6}" \
320-
--duration-ms 0 --fallback-reason "" --impl-model-tag "${impl_model_tag:-absent}" 2>/dev/null || true
321-
route → Task tool on primary
329+
# Note: the 'impl_model is absent' path is now consolidated into the 'secondary OR absent'
330+
# branch above per DEC-72. The 'primary' branch at the top of this matrix handles the
331+
# explicit opt-out. There is no separate 'absent → primary' path anymore — absence means
332+
# "follow env-driven default" which is secondary when env is configured, primary otherwise.
322333
```
323334
324-
The routing helper is implemented in `.gaai/core/adapters/claude-code/impl-routing.js` (`resolveImplRouting(implModelTag)`) — testable without invoking a real subprocess.
335+
The routing helper is implemented in `.gaai/core/adapters/claude-code/impl-routing.js` (`resolveImplRouting(implModelTag)`) — testable without invoking a real subprocess. Default routing semantics per DEC-72 (2026-04-20).
325336
326337
**Compliance:** no specific provider names appear in this workflow file. Only generic terms: "secondary provider", "nested subprocess", "user-configured endpoint" (AC13).
327338

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
---
99

10+
## [2.19.0] - 2026-04-20
11+
12+
### Changed
13+
- feat(impl-routing): DEC-72 — env-driven default (secondary when configured)
14+
- chore: bump local VERSION to v2.19.0 [sync]
15+
- feat(delivery-loop): §7c unify non-.gaai deletions into sub-agent reviewer
16+
17+
1018
## [2.19.0] - 2026-04-20
1119

1220
### Changed

0 commit comments

Comments
 (0)