@event4u/agent-config 14.19.0 → 14.21.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +351 -156
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/agent-handoff.md +42 -41
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +29 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
- package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_conformance.js +88 -110
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +42 -12
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +62 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +3801 -2803
- package/dist/install/install.mjs +396 -168
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +809 -180
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/llms.txt +14 -14
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/gate-coverage.yml +55 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +4 -3
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/blocked_by_marker.ts +189 -0
- package/src/scripts/_lib/host_capability.ts +76 -1
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/structured_ask.ts +117 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +163 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_branch_work_committed.ts +234 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +0 -157
- package/src/scripts/council_cli.ts +18 -27
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +31 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/install-hooks.sh +52 -0
- package/src/scripts/install.ts +4 -4
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_roadmap_blockers.ts +51 -1
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/migrate_command_suggestions.ts +1 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/release.ts +50 -95
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_publication.ts +40 -31
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/self_review_gate.ts +409 -15
- package/src/scripts/skill_scout.ts +69 -0
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous terminal prompting for the release pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Its own module because `release.ts` sits 371 lines over the 1500-line source
|
|
5
|
+
* ratchet, where every added line is one unit of excess — and because a SECOND
|
|
6
|
+
* caller now needs these: `guard_release_curation` in `release_publication.ts`
|
|
7
|
+
* collects the governance answer it used to only refuse over.
|
|
8
|
+
*
|
|
9
|
+
* Extracted verbatim from `release.ts`, where the `/dev/tty` discipline below
|
|
10
|
+
* was established against a reproduced failure. Do not "simplify" it back to
|
|
11
|
+
* `readSync(0)`; the comment on `promptLine` says what that costs.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import process from 'node:process';
|
|
15
|
+
|
|
16
|
+
/** Can we prompt at all — is fd 0 a TTY, or is a controlling terminal openable? */
|
|
17
|
+
export function canPrompt(): boolean {
|
|
18
|
+
if (process.env.CI) return false; // CI is non-interactive by contract → require --yes
|
|
19
|
+
if (process.stdin.isTTY) return true;
|
|
20
|
+
try {
|
|
21
|
+
const fd = fs.openSync('/dev/tty', 'r');
|
|
22
|
+
fs.closeSync(fd);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mirror of Python `input(prompt)` — write the prompt, read one line from the
|
|
31
|
+
* controlling terminal. Returns `null` when no terminal is reachable.
|
|
32
|
+
*
|
|
33
|
+
* The answer is ALWAYS read from a freshly opened, blocking `/dev/tty`
|
|
34
|
+
* (`openSync(..., 'rs')`), NEVER from fd 0. `task release` / `./scripts-run`
|
|
35
|
+
* run this under go-task's `interactive: true`, which leaves fd 0 a TTY
|
|
36
|
+
* (`process.stdin.isTTY === true`) but one Node treats as NON-blocking — so a
|
|
37
|
+
* bare `readSync(0)` throws `EAGAIN`, the catch swallows it, and the `[y/N]`
|
|
38
|
+
* prompt "auto-aborts" without ever waiting (the exact failure this fixes;
|
|
39
|
+
* reproduced with `isTTY === true`). A fresh blocking `/dev/tty` descriptor
|
|
40
|
+
* blocks for real input regardless of how the script was invoked — the proven
|
|
41
|
+
* synchronous-prompt pattern.
|
|
42
|
+
*/
|
|
43
|
+
export function promptLine(prompt: string): string | null {
|
|
44
|
+
process.stdout.write(prompt);
|
|
45
|
+
let fd: number;
|
|
46
|
+
try {
|
|
47
|
+
// 'rs' → O_RDONLY | O_SYNC: a fresh, blocking descriptor on the
|
|
48
|
+
// controlling terminal, unaffected by Node's non-blocking fd 0.
|
|
49
|
+
fd = fs.openSync('/dev/tty', 'rs');
|
|
50
|
+
} catch {
|
|
51
|
+
return null; // no controlling terminal (true non-interactive)
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const buf = Buffer.alloc(1);
|
|
55
|
+
const chars: number[] = [];
|
|
56
|
+
for (;;) {
|
|
57
|
+
let bytesRead: number;
|
|
58
|
+
try {
|
|
59
|
+
bytesRead = fs.readSync(fd, buf, 0, 1, null);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
62
|
+
// A blocking /dev/tty should not yield these, but inherited
|
|
63
|
+
// descriptor flags can — retry rather than abort the prompt.
|
|
64
|
+
if (code === 'EAGAIN' || code === 'EINTR') continue;
|
|
65
|
+
break; // EOF / EIO → EOFError analogue; return what we have.
|
|
66
|
+
}
|
|
67
|
+
if (bytesRead === 0) break;
|
|
68
|
+
const b = buf[0] as number;
|
|
69
|
+
if (b === 0x0a) break; // newline terminates the line (stripped, like input()).
|
|
70
|
+
chars.push(b);
|
|
71
|
+
}
|
|
72
|
+
return Buffer.from(chars).toString('utf-8');
|
|
73
|
+
} finally {
|
|
74
|
+
fs.closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The council's synchronous HTTP transport, and the per-provider shims that
|
|
3
|
+
* turn it into the SDK-shaped callables `clients.ts::ask()` reads.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `clients.ts` (road-to-admissible-council-seats 1.1). Two
|
|
6
|
+
* reasons, and only the first is about tidiness:
|
|
7
|
+
*
|
|
8
|
+
* 1. `clients.ts` is ~1,370 lines past the 1,500-line ceiling
|
|
9
|
+
* `check_source_size_budget` ratchets, so every line added there costs one
|
|
10
|
+
* excess line. The three shims below are new code; putting them in a file
|
|
11
|
+
* UNDER the ceiling is what lets 1.1 land without raising a baseline.
|
|
12
|
+
* 2. Before 1.1 exactly one provider (OpenAI) had a live transport and the
|
|
13
|
+
* other four constructors threw `… package not installed. pip install …`
|
|
14
|
+
* after the api-key check — so `mode: api` was dead for gemini, xai and
|
|
15
|
+
* perplexity. A shared shim module is the honest home for "how this
|
|
16
|
+
* package actually reaches a provider over HTTP", rather than four
|
|
17
|
+
* copies of the same curl call inside four constructors.
|
|
18
|
+
*
|
|
19
|
+
* The injectable seam. Every shim takes a `JsonPost` rather than calling `curlJsonPost` directly.
|
|
20
|
+
* A test can then construct a REAL client with a REAL api_key — exercising
|
|
21
|
+
* the shim-building path a mock `client:` object skips entirely — and still
|
|
22
|
+
* make no network call. That is the difference between testing the transport
|
|
23
|
+
* and testing around it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
|
|
28
|
+
/** The one network primitive. Swappable so tests never reach the network. */
|
|
29
|
+
export type JsonPost = (url: string, extraHeaders: string[], body: unknown) => unknown;
|
|
30
|
+
|
|
31
|
+
export const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Synchronous JSON POST via `curl` (no Node SDK, no Python). Matches the
|
|
35
|
+
* council's `spawnSync` transport model so the live-call clients stay
|
|
36
|
+
* synchronous. Returns the parsed response JSON (the provider HTTP APIs return
|
|
37
|
+
* exactly the SDK response shape `ask()` reads). Throws on transport failure or
|
|
38
|
+
* a non-2xx status so `ask()`'s catch surfaces it as a member error.
|
|
39
|
+
*/
|
|
40
|
+
export function curlJsonPost(url: string, extraHeaders: string[], body: unknown): unknown {
|
|
41
|
+
const args = ['-sS', '-X', 'POST', url, '-H', 'content-type: application/json'];
|
|
42
|
+
for (const h of extraHeaders) {
|
|
43
|
+
args.push('-H', h);
|
|
44
|
+
}
|
|
45
|
+
// `--connect-timeout` fast-fails a dead host; `--max-time` lets curl abort
|
|
46
|
+
// itself cleanly (surfacing a real `curl exited` error with stderr) ~10s
|
|
47
|
+
// before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
|
|
48
|
+
// A full 16k-token generation legitimately runs several minutes, so the
|
|
49
|
+
// ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
|
|
50
|
+
args.push('--connect-timeout', '30', '--max-time', '290');
|
|
51
|
+
args.push('-w', '\n%{http_code}', '--data-binary', '@-');
|
|
52
|
+
const r = spawnSync('curl', args, {
|
|
53
|
+
input: JSON.stringify(body),
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
56
|
+
timeout: 300_000,
|
|
57
|
+
});
|
|
58
|
+
if (r.error) {
|
|
59
|
+
throw new Error(`curl spawn failed: ${(r.error as Error).message}`);
|
|
60
|
+
}
|
|
61
|
+
if (r.status !== 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`curl exited ${r.status ?? 'null'}: ${(r.stderr ?? '').toString().slice(0, 500)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const out = (r.stdout ?? '').toString();
|
|
67
|
+
const nl = out.lastIndexOf('\n');
|
|
68
|
+
const httpCode = (nl >= 0 ? out.slice(nl + 1) : out).trim();
|
|
69
|
+
const bodyText = nl >= 0 ? out.slice(0, nl) : out;
|
|
70
|
+
if (!/^2\d\d$/.test(httpCode)) {
|
|
71
|
+
throw new Error(`HTTP ${httpCode}: ${bodyText.slice(0, 500)}`);
|
|
72
|
+
}
|
|
73
|
+
return JSON.parse(bodyText);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `chat.completions.create` over any OpenAI-compatible `/v1/chat/completions`.
|
|
78
|
+
* Used by OpenAI itself and by the two OpenAI-compatible vendors (xAI,
|
|
79
|
+
* Perplexity), whose only difference is the base URL.
|
|
80
|
+
*
|
|
81
|
+
* `baseUrl` is passed rather than read off `this.base_url`: a subclass field
|
|
82
|
+
* initializer (`override base_url = XAI_BASE_URL`) runs AFTER the base
|
|
83
|
+
* constructor, so reading the field during construction would silently build
|
|
84
|
+
* every compatible client against the empty string.
|
|
85
|
+
*/
|
|
86
|
+
export function openAiCompatibleClient(
|
|
87
|
+
baseUrl: string,
|
|
88
|
+
apiKey: string,
|
|
89
|
+
post: JsonPost = curlJsonPost,
|
|
90
|
+
): unknown {
|
|
91
|
+
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
92
|
+
return {
|
|
93
|
+
chat: {
|
|
94
|
+
completions: {
|
|
95
|
+
create: (kwargs: Record<string, unknown>): unknown =>
|
|
96
|
+
post(url, [`authorization: Bearer ${apiKey}`], kwargs),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `models.generate_content` over the Gemini REST API, normalised to the
|
|
104
|
+
* `google-genai` SDK response shape `GeminiClient.ask()` reads.
|
|
105
|
+
*
|
|
106
|
+
* The normalisation is the whole point and is not cosmetic: REST answers with
|
|
107
|
+
* `candidates[0].content.parts[*].text` and `usageMetadata.promptTokenCount`,
|
|
108
|
+
* while `ask()` reads `.text`, `.model_version` and
|
|
109
|
+
* `.usage_metadata.{prompt_token_count, candidates_token_count}`. Rewriting
|
|
110
|
+
* `ask()` to read REST instead would break every injected SDK-shaped mock in
|
|
111
|
+
* the existing tests, so the adapter lives here, at the transport boundary,
|
|
112
|
+
* where a shape mismatch is a transport concern.
|
|
113
|
+
*/
|
|
114
|
+
export function geminiRestClient(
|
|
115
|
+
apiKey: string,
|
|
116
|
+
post: JsonPost = curlJsonPost,
|
|
117
|
+
baseUrl: string = GEMINI_BASE_URL,
|
|
118
|
+
): unknown {
|
|
119
|
+
return {
|
|
120
|
+
models: {
|
|
121
|
+
generate_content: (kwargs: Record<string, unknown>): unknown => {
|
|
122
|
+
const model = String(kwargs['model'] ?? '');
|
|
123
|
+
const cfg = (kwargs['config'] ?? {}) as Record<string, unknown>;
|
|
124
|
+
const url = `${baseUrl.replace(/\/+$/, '')}/models/${model}:generateContent`;
|
|
125
|
+
const raw = post(
|
|
126
|
+
url,
|
|
127
|
+
[`x-goog-api-key: ${apiKey}`],
|
|
128
|
+
{
|
|
129
|
+
contents: [{ role: 'user', parts: [{ text: String(kwargs['contents'] ?? '') }] }],
|
|
130
|
+
generationConfig: { maxOutputTokens: cfg['max_output_tokens'] ?? null },
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
return normaliseGeminiRest(raw);
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** REST envelope → the `google-genai` SDK response shape. Exported for tests. */
|
|
140
|
+
export function normaliseGeminiRest(raw: unknown): {
|
|
141
|
+
text: string;
|
|
142
|
+
model_version: string;
|
|
143
|
+
usage_metadata: { prompt_token_count: number; candidates_token_count: number };
|
|
144
|
+
} {
|
|
145
|
+
const env = (raw ?? {}) as Record<string, unknown>;
|
|
146
|
+
const candidates = Array.isArray(env['candidates']) ? (env['candidates'] as unknown[]) : [];
|
|
147
|
+
const first = (candidates[0] ?? {}) as Record<string, unknown>;
|
|
148
|
+
const content = (first['content'] ?? {}) as Record<string, unknown>;
|
|
149
|
+
const parts = Array.isArray(content['parts']) ? (content['parts'] as unknown[]) : [];
|
|
150
|
+
const text = parts
|
|
151
|
+
.map((p) => String(((p ?? {}) as Record<string, unknown>)['text'] ?? ''))
|
|
152
|
+
.join('');
|
|
153
|
+
const usage = (env['usageMetadata'] ?? {}) as Record<string, unknown>;
|
|
154
|
+
const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
155
|
+
return {
|
|
156
|
+
text,
|
|
157
|
+
model_version: String(env['modelVersion'] ?? ''),
|
|
158
|
+
usage_metadata: {
|
|
159
|
+
prompt_token_count: num(usage['promptTokenCount']),
|
|
160
|
+
candidates_token_count: num(usage['candidatesTokenCount']),
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -47,6 +47,12 @@ import * as path from 'node:path';
|
|
|
47
47
|
|
|
48
48
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
49
49
|
import { boundFor } from './cli_agency_bounds.js';
|
|
50
|
+
import {
|
|
51
|
+
curlJsonPost as _curlJsonPost,
|
|
52
|
+
geminiRestClient,
|
|
53
|
+
openAiCompatibleClient,
|
|
54
|
+
type JsonPost,
|
|
55
|
+
} from './api_transport.js';
|
|
50
56
|
import { SESSION_ROLE_ENV } from '../_lib/session_role.js';
|
|
51
57
|
import { load_agent_settings } from '../_lib/agent_settings.js';
|
|
52
58
|
|
|
@@ -163,9 +169,8 @@ export const DEFAULT_OPENAI_CLI_MODEL = OPENAI_CLI_VENDOR_DEFAULT;
|
|
|
163
169
|
export const DEFAULT_ANTHROPIC_CLI_MODEL = 'sonnet';
|
|
164
170
|
export const DEFAULT_GEMINI_CLI_MODEL = 'gemini-2.5-pro';
|
|
165
171
|
|
|
166
|
-
// OpenAI-API-compatible endpoints
|
|
167
|
-
// `/v1/chat/completions` shape,
|
|
168
|
-
// custom `base_url`. Gemini has its own SDK (`google-genai`).
|
|
172
|
+
// OpenAI-API-compatible endpoints — xAI and Perplexity both expose the
|
|
173
|
+
// `/v1/chat/completions` shape, reached by one curl shim in `api_transport.ts`.
|
|
169
174
|
export const XAI_BASE_URL = 'https://api.x.ai/v1';
|
|
170
175
|
export const PERPLEXITY_BASE_URL = 'https://api.perplexity.ai';
|
|
171
176
|
|
|
@@ -461,6 +466,12 @@ interface ApiClientOptions {
|
|
|
461
466
|
// Phase 4). Default '5m' when omitted — see `DEFAULT_PROMPT_CACHE_TTL`.
|
|
462
467
|
// Only meaningful when `enable_prompt_cache` is true.
|
|
463
468
|
prompt_cache_ttl?: PromptCacheTtl | undefined;
|
|
469
|
+
/**
|
|
470
|
+
* HTTP seam. Omitted → the live `curl` post. A test passes a double
|
|
471
|
+
* TOGETHER with a real `api_key`, which exercises the shim-building
|
|
472
|
+
* path that an injected `client:` object skips entirely.
|
|
473
|
+
*/
|
|
474
|
+
transport?: JsonPost | undefined;
|
|
464
475
|
}
|
|
465
476
|
|
|
466
477
|
/** Shared ctor-options shape for the CLI clients. */
|
|
@@ -479,47 +490,6 @@ interface CliClientOptions {
|
|
|
479
490
|
consumer?: string | undefined;
|
|
480
491
|
}
|
|
481
492
|
|
|
482
|
-
/**
|
|
483
|
-
* Synchronous JSON POST via `curl` (no Node SDK, no Python). Matches the
|
|
484
|
-
* council's `spawnSync` transport model so the live-call clients stay
|
|
485
|
-
* synchronous. Returns the parsed response JSON (the provider HTTP APIs return
|
|
486
|
-
* exactly the SDK response shape `ask()` reads). Throws on transport failure or
|
|
487
|
-
* a non-2xx status so `ask()`'s catch surfaces it as a member error.
|
|
488
|
-
*/
|
|
489
|
-
function _curlJsonPost(url: string, extraHeaders: string[], body: unknown): unknown {
|
|
490
|
-
const args = ['-sS', '-X', 'POST', url, '-H', 'content-type: application/json'];
|
|
491
|
-
for (const h of extraHeaders) {
|
|
492
|
-
args.push('-H', h);
|
|
493
|
-
}
|
|
494
|
-
// `--connect-timeout` fast-fails a dead host; `--max-time` lets curl abort
|
|
495
|
-
// itself cleanly (surfacing a real `curl exited` error with stderr) ~10s
|
|
496
|
-
// before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
|
|
497
|
-
// A full 16k-token generation legitimately runs several minutes, so the
|
|
498
|
-
// ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
|
|
499
|
-
args.push('--connect-timeout', '30', '--max-time', '290');
|
|
500
|
-
args.push('-w', '\n%{http_code}', '--data-binary', '@-');
|
|
501
|
-
const r = spawnSync('curl', args, {
|
|
502
|
-
input: JSON.stringify(body),
|
|
503
|
-
encoding: 'utf8',
|
|
504
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
505
|
-
timeout: 300_000,
|
|
506
|
-
});
|
|
507
|
-
if (r.error) {
|
|
508
|
-
throw new Error(`curl spawn failed: ${(r.error as Error).message}`);
|
|
509
|
-
}
|
|
510
|
-
if (r.status !== 0) {
|
|
511
|
-
throw new Error(`curl exited ${r.status ?? 'null'}: ${(r.stderr ?? '').toString().slice(0, 500)}`);
|
|
512
|
-
}
|
|
513
|
-
const out = (r.stdout ?? '').toString();
|
|
514
|
-
const nl = out.lastIndexOf('\n');
|
|
515
|
-
const httpCode = (nl >= 0 ? out.slice(nl + 1) : out).trim();
|
|
516
|
-
const bodyText = nl >= 0 ? out.slice(0, nl) : out;
|
|
517
|
-
if (!/^2\d\d$/.test(httpCode)) {
|
|
518
|
-
throw new Error(`HTTP ${httpCode}: ${bodyText.slice(0, 500)}`);
|
|
519
|
-
}
|
|
520
|
-
return JSON.parse(bodyText);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
493
|
export class AnthropicClient extends ExternalAIClient {
|
|
524
494
|
override name = 'anthropic';
|
|
525
495
|
override billable = true;
|
|
@@ -824,13 +794,12 @@ export class OpenAIClient extends ExternalAIClient {
|
|
|
824
794
|
// ── Gemini / xAI / Perplexity (Phase 0 — Step 6) ─────────────────────
|
|
825
795
|
|
|
826
796
|
/**
|
|
827
|
-
* Google Gemini
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
* `
|
|
832
|
-
*
|
|
833
|
-
* `.usage_metadata.{prompt_token_count, candidates_token_count}`.
|
|
797
|
+
* Google Gemini over the REST API (1.1 — there is no SDK dependency; the old
|
|
798
|
+
* `google-genai` lazy-import never existed in the TS port and the constructor
|
|
799
|
+
* simply threw). `ask()` still reads the SDK RESPONSE shape — `.text`,
|
|
800
|
+
* `.model_version`, `.usage_metadata.{prompt_token_count,
|
|
801
|
+
* candidates_token_count}` — which `geminiRestClient` normalises to, so every
|
|
802
|
+
* injected-mock test is unaffected.
|
|
834
803
|
*/
|
|
835
804
|
export class GeminiClient extends ExternalAIClient {
|
|
836
805
|
override name = 'gemini';
|
|
@@ -851,7 +820,12 @@ export class GeminiClient extends ExternalAIClient {
|
|
|
851
820
|
'Use `api_key_ref: env:GEMINI_API_KEY` in ~/.event4u/agent-config/settings/.ai-council.yml.',
|
|
852
821
|
);
|
|
853
822
|
}
|
|
854
|
-
|
|
823
|
+
// Live transport (1.1). Was an unconditional `pip install google-genai`
|
|
824
|
+
// throw, which made `mode: api` dead for this provider however the key
|
|
825
|
+
// was configured. `geminiRestClient` normalises the REST envelope into
|
|
826
|
+
// the SDK shape `ask()` below already reads, so `ask()` is untouched
|
|
827
|
+
// and every existing injected-mock test keeps passing.
|
|
828
|
+
this._client = geminiRestClient(api_key, opts.transport ?? _curlJsonPost);
|
|
855
829
|
}
|
|
856
830
|
|
|
857
831
|
override ask(
|
|
@@ -901,19 +875,26 @@ export class GeminiClient extends ExternalAIClient {
|
|
|
901
875
|
}
|
|
902
876
|
|
|
903
877
|
/**
|
|
904
|
-
* Shared shape for OpenAI-API-compatible providers (xAI, Perplexity)
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
* that requires `max_completion_tokens` as of 2026-05-14.
|
|
878
|
+
* Shared shape for OpenAI-API-compatible providers (xAI, Perplexity): both
|
|
879
|
+
* implement `/v1/chat/completions`, so 1.1 reaches them with one curl shim and
|
|
880
|
+
* a base URL. The reasoning-model branch from `OpenAIClient` is intentionally
|
|
881
|
+
* omitted — neither ships a reasoning model that requires
|
|
882
|
+
* `max_completion_tokens` as of 2026-05-14.
|
|
910
883
|
*/
|
|
911
884
|
export class _OpenAICompatibleClient extends ExternalAIClient {
|
|
912
885
|
override billable = true;
|
|
913
886
|
base_url = '';
|
|
914
887
|
protected _client: unknown;
|
|
915
888
|
|
|
916
|
-
constructor(opts: {
|
|
889
|
+
constructor(opts: {
|
|
890
|
+
model: string;
|
|
891
|
+
client?: unknown;
|
|
892
|
+
api_key?: string | null | undefined;
|
|
893
|
+
transport?: JsonPost | undefined;
|
|
894
|
+
// Passed, never read off `this.base_url`: a subclass field initializer
|
|
895
|
+
// runs AFTER this constructor, so the field is still '' right here.
|
|
896
|
+
base_url: string;
|
|
897
|
+
}) {
|
|
917
898
|
super();
|
|
918
899
|
this.model = opts.model;
|
|
919
900
|
if (opts.client !== undefined && opts.client !== null) {
|
|
@@ -926,7 +907,13 @@ export class _OpenAICompatibleClient extends ExternalAIClient {
|
|
|
926
907
|
`${this.constructor.name} requires explicit api_key or injected client.`,
|
|
927
908
|
);
|
|
928
909
|
}
|
|
929
|
-
|
|
910
|
+
// Live transport (1.1) — replaces an unconditional `pip install openai`
|
|
911
|
+
// throw that made `mode: api` dead for xai and perplexity.
|
|
912
|
+
this._client = openAiCompatibleClient(
|
|
913
|
+
opts.base_url,
|
|
914
|
+
api_key,
|
|
915
|
+
opts.transport ?? _curlJsonPost,
|
|
916
|
+
);
|
|
930
917
|
}
|
|
931
918
|
|
|
932
919
|
override ask(
|
|
@@ -989,7 +976,13 @@ export class XAIClient extends _OpenAICompatibleClient {
|
|
|
989
976
|
override base_url = XAI_BASE_URL;
|
|
990
977
|
|
|
991
978
|
constructor(opts: ApiClientOptions = {}) {
|
|
992
|
-
super({
|
|
979
|
+
super({
|
|
980
|
+
model: opts.model ?? DEFAULT_XAI_MODEL,
|
|
981
|
+
client: opts.client,
|
|
982
|
+
api_key: opts.api_key,
|
|
983
|
+
transport: opts.transport,
|
|
984
|
+
base_url: XAI_BASE_URL,
|
|
985
|
+
});
|
|
993
986
|
}
|
|
994
987
|
}
|
|
995
988
|
|
|
@@ -1003,6 +996,8 @@ export class PerplexityClient extends _OpenAICompatibleClient {
|
|
|
1003
996
|
model: opts.model ?? DEFAULT_PERPLEXITY_MODEL,
|
|
1004
997
|
client: opts.client,
|
|
1005
998
|
api_key: opts.api_key,
|
|
999
|
+
transport: opts.transport,
|
|
1000
|
+
base_url: PERPLEXITY_BASE_URL,
|
|
1006
1001
|
});
|
|
1007
1002
|
}
|
|
1008
1003
|
}
|
|
@@ -1028,8 +1023,8 @@ export {
|
|
|
1028
1023
|
//
|
|
1029
1024
|
// Raised 120 → 300 (2026-08-13) to match the API transport, which has carried
|
|
1030
1025
|
// `--max-time 290` / `timeout: 300_000` since the 2026-06-24 repair of the same
|
|
1031
|
-
// symptom. That repair landed in `
|
|
1032
|
-
//
|
|
1026
|
+
// symptom. That repair landed in `api_transport.ts::curlJsonPost` only, so with
|
|
1027
|
+
// a member on `cli · subscription` the old cap was still live — a deep run
|
|
1033
1028
|
// reproduced it exactly: both members returned `error: timeout` at
|
|
1034
1029
|
// `latency_ms: 122921` with `timeout_seconds: 120`, and the run reported
|
|
1035
1030
|
// `0/2 present — INCONCLUSIVE`.
|
|
@@ -83,6 +83,8 @@ import * as budget from './cli_call_budget.js';
|
|
|
83
83
|
// Converting it to a value import — or adding any other runtime import from
|
|
84
84
|
// quorum into config — creates a real ESM cycle in the loader's init path.
|
|
85
85
|
import { OPENAI_CLI_VENDOR_DEFAULT } from './clients.js';
|
|
86
|
+
import { parseSeatFields, type PolicyExclusion } from './seat_policy.js';
|
|
87
|
+
import type { ContentClass } from './content_ceiling.js';
|
|
86
88
|
import { SOLO_FLOOR_MIN_PRESENT } from './quorum.js';
|
|
87
89
|
|
|
88
90
|
const _VALID_PROVIDERS: ReadonlySet<string> = new Set([
|
|
@@ -330,6 +332,12 @@ export interface MemberConfig {
|
|
|
330
332
|
* demanding a date nobody would refresh.
|
|
331
333
|
*/
|
|
332
334
|
readonly verified_at: string | null;
|
|
335
|
+
/** Seat policy — parsed and documented by `seat_policy.ts::parseSeatFields`.
|
|
336
|
+
* All three are NOTES or REFUSALS: none can make a seat available that was
|
|
337
|
+
* not already available. */
|
|
338
|
+
readonly disabled_reason: string | null;
|
|
339
|
+
readonly policy_exclusion: PolicyExclusion | null;
|
|
340
|
+
readonly content_ceiling: ContentClass;
|
|
333
341
|
}
|
|
334
342
|
|
|
335
343
|
/**
|
|
@@ -1875,6 +1883,10 @@ function _build_member(
|
|
|
1875
1883
|
}
|
|
1876
1884
|
verified_at = coerced;
|
|
1877
1885
|
}
|
|
1886
|
+
const seat = parseSeatFields(name, cfg as Record<string, unknown>);
|
|
1887
|
+
if ('error' in seat) {
|
|
1888
|
+
throw new CouncilConfigError(seat.error);
|
|
1889
|
+
}
|
|
1878
1890
|
const prompt_cache_raw = _get(cfg, 'prompt_cache', null);
|
|
1879
1891
|
let prompt_cache_ttl: '5m' | '1h' = '5m';
|
|
1880
1892
|
if (prompt_cache_raw !== null && prompt_cache_raw !== undefined && !_isBool(prompt_cache_raw)) {
|
|
@@ -1907,6 +1919,7 @@ function _build_member(
|
|
|
1907
1919
|
tier,
|
|
1908
1920
|
prompt_cache_ttl,
|
|
1909
1921
|
verified_at,
|
|
1922
|
+
...seat.fields,
|
|
1910
1923
|
};
|
|
1911
1924
|
}
|
|
1912
1925
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One binary content ceiling per seat — road-to-admissible-council-seats 2.2.
|
|
3
|
+
*
|
|
4
|
+
* Redaction today is a SECRETS floor and says so
|
|
5
|
+
* (`src/skills/ai-council/references/cost-and-redaction.md`). Nothing above it
|
|
6
|
+
* could express "this project's diff may not go to that seat", which is a
|
|
7
|
+
* different question with a different answer: a diff carrying no secret at all
|
|
8
|
+
* is still the consumer's private source.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately BINARY. A graded scale invites a middle value nobody can define
|
|
11
|
+
* and every caller reads differently; two values force the decision at
|
|
12
|
+
* authoring time, where it belongs.
|
|
13
|
+
*
|
|
14
|
+
* public-artifact — repo-owned artefacts: skill descriptions, the trigger
|
|
15
|
+
* corpus, rule text, bench fixtures. Already public.
|
|
16
|
+
* project-content — everything a consumer supplies. The DEFAULT, so an
|
|
17
|
+
* unclassified payload is treated as private rather than
|
|
18
|
+
* as publishable.
|
|
19
|
+
*
|
|
20
|
+
* A seat's ceiling is the most sensitive class it may receive. `public-artifact`
|
|
21
|
+
* is therefore the NARROWER seat: it admits public artefacts only.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const CONTENT_CLASSES = ['public-artifact', 'project-content'] as const;
|
|
25
|
+
export type ContentClass = (typeof CONTENT_CLASSES)[number];
|
|
26
|
+
|
|
27
|
+
/** The default for anything not explicitly classified: private, not public. */
|
|
28
|
+
export const DEFAULT_CONTENT_CLASS: ContentClass = 'project-content';
|
|
29
|
+
|
|
30
|
+
/** The default seat ceiling: unchanged behaviour for every existing seat. */
|
|
31
|
+
export const DEFAULT_SEAT_CEILING: ContentClass = 'project-content';
|
|
32
|
+
|
|
33
|
+
const _SET: ReadonlySet<string> = new Set<string>(CONTENT_CLASSES);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Any configured value → a content class. Unrecognised input becomes
|
|
37
|
+
* `project-content`, the RESTRICTIVE value: a typo must narrow what a seat may
|
|
38
|
+
* receive, never widen it.
|
|
39
|
+
*/
|
|
40
|
+
export function classifyContentClass(raw: unknown, fallback: ContentClass = DEFAULT_CONTENT_CLASS): ContentClass {
|
|
41
|
+
if (typeof raw === 'string' && _SET.has(raw)) {
|
|
42
|
+
return raw as ContentClass;
|
|
43
|
+
}
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CeilingVerdict {
|
|
48
|
+
readonly admitted: boolean;
|
|
49
|
+
/** Non-null exactly when `admitted` is false. */
|
|
50
|
+
readonly reason: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Does a seat whose ceiling is `seatCeiling` admit a payload of `contentClass`?
|
|
55
|
+
*
|
|
56
|
+
* The one refusal is `project-content` into a `public-artifact` seat. Every
|
|
57
|
+
* other pairing is admitted, including public artefacts into a
|
|
58
|
+
* `project-content` seat — a seat cleared for private content is trivially
|
|
59
|
+
* cleared for public content, and refusing that would be a nonsense that
|
|
60
|
+
* disables the default seat for its own repo's fixtures.
|
|
61
|
+
*
|
|
62
|
+
* Refusal is a GREEN path (this roadmap's 2.2 verify): the caller drops the
|
|
63
|
+
* seat and the run continues. It is not an error, because a policy working as
|
|
64
|
+
* designed is not a failure.
|
|
65
|
+
*/
|
|
66
|
+
export function admitsContent(
|
|
67
|
+
seatCeiling: ContentClass,
|
|
68
|
+
contentClass: ContentClass,
|
|
69
|
+
): CeilingVerdict {
|
|
70
|
+
if (seatCeiling === 'public-artifact' && contentClass === 'project-content') {
|
|
71
|
+
return {
|
|
72
|
+
admitted: false,
|
|
73
|
+
reason:
|
|
74
|
+
"seat ceiling is 'public-artifact'; the payload is classified " +
|
|
75
|
+
"'project-content' (consumer-supplied)",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { admitted: true, reason: null };
|
|
79
|
+
}
|