@event4u/agent-config 9.7.0 → 9.8.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +144 -0
- package/README.md +5 -5
- package/dist/agent-src/commands/commit/in-chunks.md +8 -0
- package/dist/agent-src/commands/commit.md +17 -0
- package/dist/agent-src/commands/pr/create.md +4 -0
- package/dist/agent-src/contexts/override-system.md +75 -5
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
- package/dist/agent-src/rules/domain-safety-pii.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +2 -0
- package/dist/agent-src/rules/language-and-tone.md +2 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +3 -0
- package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
- package/dist/agent-src/rules/onboarding-gate.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +3 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/telegraph-speak.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/verify-before-complete.md +2 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
- package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
- package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
- package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
- package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
- package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
- package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
- package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
- package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
- package/dist/cli/configRoot.js +112 -0
- package/dist/cli/configRoot.js.map +1 -0
- package/dist/cli/configRoot.test.js +97 -0
- package/dist/cli/configRoot.test.js.map +1 -0
- package/dist/cli/help.js +4 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +19 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +161 -44
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -8
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +11 -6
- package/dist/install/install.mjs +16 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/server/app.js +10 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/routes/ping.js +18 -0
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/schemas/settings.js +8 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/server/serverInfo.js +6 -2
- package/dist/server/serverInfo.js.map +1 -1
- package/dist/server/token.js +11 -4
- package/dist/server/token.js.map +1 -1
- package/dist/server/writeRoot.js +15 -1
- package/dist/server/writeRoot.js.map +1 -1
- package/dist/shared/capabilities.js +33 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/capabilities.test.js +39 -0
- package/dist/shared/capabilities.test.js.map +1 -0
- package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
- package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
- package/dist/ui/index.html +14 -5
- package/docs/CLAIMS.md +62 -37
- package/docs/architecture.md +2 -2
- package/docs/benchmark.md +137 -0
- package/docs/catalog.md +8 -3
- package/docs/comparison.yaml +44 -2
- package/docs/contracts/local-server-ports.md +53 -0
- package/docs/contracts/no-runtime-boundary.md +1 -1
- package/docs/contracts/subagent-boundary.md +15 -0
- package/docs/customization.md +14 -3
- package/docs/decisions/ADR-016-installer-architecture.md +2 -0
- package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
- package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
- package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
- package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
- package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
- package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
- package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
- package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
- package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
- package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
- package/docs/decisions/INDEX.md +21 -0
- package/docs/decisions/engine-reclassification-2026-07.md +104 -0
- package/docs/distribution/awesome-list-submission.md +1 -1
- package/docs/distribution/registries.md +36 -27
- package/docs/enforcement-by-host.md +7 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guides/gated-platform-reads.md +221 -0
- package/docs/positioning-evidence.md +1 -1
- package/docs/proof.md +92 -16
- package/docs/threat-model.md +1 -0
- package/package.json +5 -2
- package/src/config/agent-settings.template.yml +43 -0
- package/src/config/reach-channels.yml +217 -0
- package/src/config/reach-prescriptions-intake.yml +200 -0
- package/src/scripts/_dispatch.bash +15 -0
- package/src/scripts/_lib/exec_evidence.ts +251 -0
- package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
- package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
- package/src/scripts/_lib/kernel_rules.ts +46 -0
- package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
- package/src/scripts/_lib/risk_paths.ts +160 -0
- package/src/scripts/_lib/secret_detector.test.ts +103 -0
- package/src/scripts/_lib/secret_detector.ts +319 -0
- package/src/scripts/_lib/tool_probe.ts +359 -0
- package/src/scripts/adr/regenerate_index.ts +81 -6
- package/src/scripts/audit_adr_coverage.ts +33 -0
- package/src/scripts/build_proof.ts +162 -0
- package/src/scripts/check_adr_frontmatter.ts +195 -0
- package/src/scripts/check_artefact_count_messaging.ts +4 -0
- package/src/scripts/check_backstop_debt.ts +200 -0
- package/src/scripts/check_claims.ts +188 -8
- package/src/scripts/check_comparison.ts +12 -0
- package/src/scripts/check_enforcement_coverage.ts +592 -0
- package/src/scripts/check_kernel_rule_bundle.ts +16 -12
- package/src/scripts/check_reach_channels.ts +407 -0
- package/src/scripts/check_reach_staleness.ts +364 -0
- package/src/scripts/check_secret_leak.test.ts +47 -0
- package/src/scripts/check_secret_leak.ts +290 -0
- package/src/scripts/code_graph/build.ts +0 -0
- package/src/scripts/code_graph/cli.ts +207 -0
- package/src/scripts/code_graph/detect.ts +134 -0
- package/src/scripts/code_graph/extract.ts +386 -0
- package/src/scripts/code_graph/loader.ts +123 -0
- package/src/scripts/code_graph/query.ts +176 -0
- package/src/scripts/code_graph/sanitize.ts +30 -0
- package/src/scripts/code_graph/types.ts +83 -0
- package/src/scripts/code_graph/validate.ts +63 -0
- package/src/scripts/external_sources_denylist.json +3 -1
- package/src/scripts/generate_subagent_floor.ts +194 -0
- package/src/scripts/hook_manifest.yaml +13 -3
- package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
- package/src/scripts/iron_law_sha.ts +3 -12
- package/src/scripts/lint_override_kernel_guard.ts +219 -0
- package/src/scripts/measure_rule_budget.ts +3 -12
- package/src/scripts/postinstall_gui.ts +129 -0
- package/src/scripts/reach_doctor.ts +1651 -0
- package/src/scripts/release.ts +24 -3
- package/src/scripts/risk_escalation_shadow.ts +160 -0
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/reach-channels.schema.json +134 -0
- package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/scripts/skill_linter.ts +55 -0
- package/src/scripts/validate_reach_prescriptions.ts +751 -0
- package/src/server/schemas/settings.ts +16 -0
- package/src/shared/capabilities.test.ts +43 -0
- package/src/shared/capabilities.ts +75 -0
- package/src/templates/minimal/agents-overrides-readme.md +20 -0
- package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool probe — five-state health classification for an upstream CLI backend
|
|
3
|
+
* (road-to-internet-reach Phase 2, step 1).
|
|
4
|
+
*
|
|
5
|
+
* The reach layer never wraps or proxies a tool; it only answers "is this
|
|
6
|
+
* backend usable right now, and if not, what does the human run to fix it?".
|
|
7
|
+
* This module is that answer for exactly ONE backend descriptor. Iterating a
|
|
8
|
+
* registry, rendering a table and choosing an active backend all belong to the
|
|
9
|
+
* caller (`reach_doctor.ts`), which follows the `hooks_doctor.ts` payload /
|
|
10
|
+
* exit-code conventions.
|
|
11
|
+
*
|
|
12
|
+
* Taxonomy (the whole contract):
|
|
13
|
+
*
|
|
14
|
+
* ok — the binary resolves AND the side-effect-free probe exits 0.
|
|
15
|
+
* missing — the binary does not resolve on PATH at all.
|
|
16
|
+
* broken — the binary resolves but the probe fails in a way that means a
|
|
17
|
+
* damaged install: exit 126 / 127, an `EACCES` on a path that
|
|
18
|
+
* passed the executable check, or a resolvable shim whose
|
|
19
|
+
* interpreter is gone (the **stale-shim** case: resolution finds
|
|
20
|
+
* the file, executing it fails `ENOENT`).
|
|
21
|
+
* timeout — the probe exceeded its per-probe deadline.
|
|
22
|
+
* error — anything else, captured and attributed to this descriptor only
|
|
23
|
+
* (including a probe that ran and exited non-zero with a code
|
|
24
|
+
* other than 126 / 127 — the tool is installed but disagrees).
|
|
25
|
+
*
|
|
26
|
+
* Non-negotiables:
|
|
27
|
+
*
|
|
28
|
+
* - **Every spawn goes through `hardenedSpawnEnv()`** (ADR-123): the probe
|
|
29
|
+
* runs external binaries, so the loader / git-config / runtime-auto-exec
|
|
30
|
+
* injection vectors are scrubbed from the child env. PATH resolution walks
|
|
31
|
+
* the *same* hardened env, so "what we resolved" and "what we executed"
|
|
32
|
+
* can never diverge.
|
|
33
|
+
* - **Side-effect-free probes only.** The caller supplies the args
|
|
34
|
+
* (`--version`, `--help`, a status subcommand). This module never invents
|
|
35
|
+
* args and never runs an install / update / login / write command.
|
|
36
|
+
* - **Retry applies to `timeout` ONLY** — exactly one retry, never for
|
|
37
|
+
* `missing`, `broken` or `error`. A missing binary will still be missing.
|
|
38
|
+
* - **Never throws.** A malformed descriptor, an invalid arg, a hostile
|
|
39
|
+
* filesystem — all become a returned `error` result. One bad backend must
|
|
40
|
+
* not take down a caller iterating over many.
|
|
41
|
+
* - **No network, no config reading.** The descriptor is passed in; the
|
|
42
|
+
* `fix` prescription is echoed from it. This module invents no install
|
|
43
|
+
* command of its own.
|
|
44
|
+
*
|
|
45
|
+
* Child stdio is fully ignored: the verdict is the exit status, capturing
|
|
46
|
+
* output would risk pulling credentials / fetched content into a report, and
|
|
47
|
+
* an ignored stdio cannot hang on a grandchild holding the pipe open after a
|
|
48
|
+
* timeout kill.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { spawnSync } from 'node:child_process';
|
|
52
|
+
import type { SpawnSyncOptions } from 'node:child_process';
|
|
53
|
+
import * as fs from 'node:fs';
|
|
54
|
+
import * as path from 'node:path';
|
|
55
|
+
|
|
56
|
+
import { hardenedSpawnEnv } from './spawn_env.js';
|
|
57
|
+
|
|
58
|
+
/** The five states. Nothing else is representable. */
|
|
59
|
+
export type ToolProbeStatus = 'ok' | 'missing' | 'broken' | 'timeout' | 'error';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A platform-keyed prescription, e.g.
|
|
63
|
+
* `{ darwin: 'brew install gh@2.62.0', default: 'see docs/…' }`.
|
|
64
|
+
* Resolved by `process.platform`, falling back to `default`. The strings are
|
|
65
|
+
* the caller's — this module only selects and echoes.
|
|
66
|
+
*/
|
|
67
|
+
export type FixPrescription = string | Readonly<Record<string, string>>;
|
|
68
|
+
|
|
69
|
+
/** One backend to probe. Registry-shaped (snake_case), never guessed. */
|
|
70
|
+
export interface ToolProbeDescriptor {
|
|
71
|
+
/** Stable id echoed into the result (channel / backend name). */
|
|
72
|
+
name: string;
|
|
73
|
+
/** Binary name resolved on PATH, or an explicit absolute / relative path. */
|
|
74
|
+
bin: string;
|
|
75
|
+
/** Side-effect-free probe args. Empty means "run the bare binary". */
|
|
76
|
+
probe_args?: readonly string[];
|
|
77
|
+
/** Per-probe deadline. Defaults to {@link DEFAULT_TIMEOUT_MS}. */
|
|
78
|
+
timeout_ms?: number;
|
|
79
|
+
/** Human-run prescription echoed on `missing` / `broken`. */
|
|
80
|
+
fix?: FixPrescription;
|
|
81
|
+
/** Env overrides handed to `hardenedSpawnEnv()` (e.g. a scoped PATH). */
|
|
82
|
+
env?: Readonly<Record<string, string>>;
|
|
83
|
+
/** Working directory for the probe. Defaults to the caller's cwd. */
|
|
84
|
+
cwd?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** JSON-serialisable verdict for exactly one descriptor. */
|
|
88
|
+
export interface ToolProbeResult {
|
|
89
|
+
name: string;
|
|
90
|
+
bin: string;
|
|
91
|
+
status: ToolProbeStatus;
|
|
92
|
+
/** Resolved binary path, or `null` when resolution failed. */
|
|
93
|
+
path: string | null;
|
|
94
|
+
/** Child exit code, or `null` when the child never reported one. */
|
|
95
|
+
exit_code: number | null;
|
|
96
|
+
/** Terminating signal, or `null`. `SIGTERM` accompanies a timeout kill. */
|
|
97
|
+
signal: string | null;
|
|
98
|
+
/** Probe invocations actually performed (2 only on the timeout retry). */
|
|
99
|
+
attempts: number;
|
|
100
|
+
/** Deadline applied per attempt. */
|
|
101
|
+
timeout_ms: number;
|
|
102
|
+
/** Short, output-free explanation of the verdict. */
|
|
103
|
+
diagnostic: string;
|
|
104
|
+
/** Echoed prescription — populated for `missing` / `broken` only. */
|
|
105
|
+
fix: string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Default per-probe deadline: generous for a cold `--version`, still bounded. */
|
|
109
|
+
export const DEFAULT_TIMEOUT_MS = 5_000;
|
|
110
|
+
|
|
111
|
+
/** Exit codes that mean "resolvable but damaged" rather than "disagrees". */
|
|
112
|
+
const BROKEN_EXIT_CODES: ReadonlySet<number> = new Set([126, 127]);
|
|
113
|
+
|
|
114
|
+
/** Resolve the platform-appropriate prescription; `null` when none was given. */
|
|
115
|
+
function resolveFix(fix: FixPrescription | undefined): string | null {
|
|
116
|
+
if (fix === undefined) return null;
|
|
117
|
+
if (typeof fix === 'string') return fix.trim() === '' ? null : fix;
|
|
118
|
+
const exact = fix[process.platform];
|
|
119
|
+
if (typeof exact === 'string' && exact.trim() !== '') return exact;
|
|
120
|
+
const fallback = fix['default'];
|
|
121
|
+
if (typeof fallback === 'string' && fallback.trim() !== '') return fallback;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isExecutableFile(candidate: string): boolean {
|
|
126
|
+
try {
|
|
127
|
+
if (!fs.statSync(candidate).isFile()) return false;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (process.platform === 'win32') {
|
|
132
|
+
// X_OK is not meaningful on Windows; extension carries executability.
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function looksLikePath(bin: string): boolean {
|
|
144
|
+
return bin.includes('/') || (process.platform === 'win32' && bin.includes('\\'));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* PATH resolution over the **hardened** env — the same env the probe will run
|
|
149
|
+
* under. Returns the resolved path, or `null` for `missing`.
|
|
150
|
+
*/
|
|
151
|
+
function resolveBinary(bin: string, env: NodeJS.ProcessEnv): string | null {
|
|
152
|
+
if (bin.trim() === '') return null;
|
|
153
|
+
if (looksLikePath(bin)) {
|
|
154
|
+
const direct = path.resolve(bin);
|
|
155
|
+
return isExecutableFile(direct) ? direct : null;
|
|
156
|
+
}
|
|
157
|
+
const pathValue = env['PATH'] ?? env['Path'] ?? '';
|
|
158
|
+
const extensions =
|
|
159
|
+
process.platform === 'win32'
|
|
160
|
+
? (env['PATHEXT'] ?? '.COM;.EXE;.BAT;.CMD').split(';').filter((e) => e !== '')
|
|
161
|
+
: [''];
|
|
162
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
163
|
+
if (dir === '') continue;
|
|
164
|
+
for (const ext of extensions) {
|
|
165
|
+
const candidate = path.join(dir, `${bin}${ext}`);
|
|
166
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface Attempt {
|
|
173
|
+
status: number | null;
|
|
174
|
+
signal: string | null;
|
|
175
|
+
errorCode: string | null;
|
|
176
|
+
timedOut: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runOnce(
|
|
180
|
+
resolved: string,
|
|
181
|
+
args: readonly string[],
|
|
182
|
+
timeoutMs: number,
|
|
183
|
+
env: NodeJS.ProcessEnv,
|
|
184
|
+
cwd: string | undefined,
|
|
185
|
+
): Attempt {
|
|
186
|
+
const options: SpawnSyncOptions = {
|
|
187
|
+
env,
|
|
188
|
+
timeout: timeoutMs,
|
|
189
|
+
// See the module header: verdict is the exit status, never the output.
|
|
190
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
};
|
|
193
|
+
if (cwd !== undefined) options.cwd = cwd;
|
|
194
|
+
const result = spawnSync(resolved, [...args], options);
|
|
195
|
+
// Node attaches the libuv code (`ENOENT` / `EACCES` / `ETIMEDOUT`) to the
|
|
196
|
+
// spawn error; it is not part of the declared `Error` shape.
|
|
197
|
+
const raw = result.error as (Error & { code?: unknown }) | undefined;
|
|
198
|
+
const errorCode = typeof raw?.code === 'string' ? raw.code : null;
|
|
199
|
+
return {
|
|
200
|
+
status: result.status ?? null,
|
|
201
|
+
signal: result.signal ?? null,
|
|
202
|
+
errorCode,
|
|
203
|
+
// Node reports a deadline kill as ETIMEDOUT; the SIGTERM+no-status
|
|
204
|
+
// shape is the belt-and-braces reading for hosts that omit the code.
|
|
205
|
+
timedOut:
|
|
206
|
+
errorCode === 'ETIMEDOUT' ||
|
|
207
|
+
(result.status === null && result.signal === 'SIGTERM' && errorCode === null),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function classify(resolved: string, attempt: Attempt, timeoutMs: number, attempts: number): {
|
|
212
|
+
status: ToolProbeStatus;
|
|
213
|
+
diagnostic: string;
|
|
214
|
+
} {
|
|
215
|
+
if (attempt.timedOut) {
|
|
216
|
+
return {
|
|
217
|
+
status: 'timeout',
|
|
218
|
+
diagnostic: `probe exceeded the ${timeoutMs}ms deadline after ${attempts} attempt(s)`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (attempt.errorCode === 'ENOENT') {
|
|
222
|
+
// Resolution found the file, exec said it is not there → the shebang
|
|
223
|
+
// interpreter is gone. This is the stale-shim case.
|
|
224
|
+
return {
|
|
225
|
+
status: 'broken',
|
|
226
|
+
diagnostic: `resolved at ${resolved} but exec failed ENOENT — stale shim (interpreter missing)`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (attempt.errorCode === 'EACCES') {
|
|
230
|
+
return {
|
|
231
|
+
status: 'broken',
|
|
232
|
+
diagnostic: `resolved at ${resolved} but exec failed EACCES — not executable as installed`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (attempt.errorCode !== null) {
|
|
236
|
+
return {
|
|
237
|
+
status: 'error',
|
|
238
|
+
diagnostic: `probe spawn failed ${attempt.errorCode}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (attempt.status === 0) {
|
|
242
|
+
return { status: 'ok', diagnostic: `probe exited 0 (${resolved})` };
|
|
243
|
+
}
|
|
244
|
+
if (attempt.status !== null && BROKEN_EXIT_CODES.has(attempt.status)) {
|
|
245
|
+
return {
|
|
246
|
+
status: 'broken',
|
|
247
|
+
diagnostic: `probe exited ${attempt.status} — damaged install (command not found / not executable inside the wrapper)`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (attempt.status !== null) {
|
|
251
|
+
return { status: 'error', diagnostic: `probe exited ${attempt.status}` };
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
status: 'error',
|
|
255
|
+
diagnostic: `probe reported no exit status${attempt.signal !== null ? ` (signal ${attempt.signal})` : ''}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Probe one backend. Returns a verdict; **never throws**.
|
|
261
|
+
*
|
|
262
|
+
* @param descriptor the backend to probe. Everything the probe needs —
|
|
263
|
+
* binary, side-effect-free args, deadline, and the `fix` prescription echoed
|
|
264
|
+
* on `missing` / `broken` — comes from here. No config or network read.
|
|
265
|
+
*/
|
|
266
|
+
export function probeTool(descriptor: ToolProbeDescriptor): ToolProbeResult {
|
|
267
|
+
// Types are a compile-time promise, not a runtime one: a descriptor built
|
|
268
|
+
// from a YAML registry can carry anything (or be nullish), so every read
|
|
269
|
+
// below is defensive and the whole body is inside the never-throw guard.
|
|
270
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
271
|
+
const base: ToolProbeResult = {
|
|
272
|
+
name: '(unnamed)',
|
|
273
|
+
bin: '',
|
|
274
|
+
status: 'error',
|
|
275
|
+
path: null,
|
|
276
|
+
exit_code: null,
|
|
277
|
+
signal: null,
|
|
278
|
+
attempts: 0,
|
|
279
|
+
timeout_ms: timeoutMs,
|
|
280
|
+
diagnostic: 'probe did not run',
|
|
281
|
+
fix: null,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const name = typeof descriptor?.name === 'string' ? descriptor.name : '(unnamed)';
|
|
286
|
+
const bin = typeof descriptor?.bin === 'string' ? descriptor.bin : '';
|
|
287
|
+
base.name = name;
|
|
288
|
+
base.bin = bin;
|
|
289
|
+
|
|
290
|
+
const requested = descriptor.timeout_ms;
|
|
291
|
+
if (typeof requested === 'number' && Number.isFinite(requested) && requested > 0) {
|
|
292
|
+
timeoutMs = Math.floor(requested);
|
|
293
|
+
}
|
|
294
|
+
base.timeout_ms = timeoutMs;
|
|
295
|
+
const fix = resolveFix(descriptor.fix);
|
|
296
|
+
|
|
297
|
+
if (bin.trim() === '') {
|
|
298
|
+
return {
|
|
299
|
+
...base,
|
|
300
|
+
status: 'error',
|
|
301
|
+
diagnostic: 'descriptor carries no binary to probe',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const env = hardenedSpawnEnv({ ...(descriptor.env ?? {}) });
|
|
306
|
+
const resolved = resolveBinary(bin, env);
|
|
307
|
+
if (resolved === null) {
|
|
308
|
+
return {
|
|
309
|
+
...base,
|
|
310
|
+
status: 'missing',
|
|
311
|
+
diagnostic: `'${bin}' does not resolve on PATH`,
|
|
312
|
+
fix,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const args = descriptor.probe_args ?? [];
|
|
317
|
+
const cwd = descriptor.cwd;
|
|
318
|
+
|
|
319
|
+
let attempts = 0;
|
|
320
|
+
let attempt = runOnce(resolved, args, timeoutMs, env, cwd);
|
|
321
|
+
attempts += 1;
|
|
322
|
+
// Retry ONLY a timeout, and only once. `missing` / `broken` / `error`
|
|
323
|
+
// are stable verdicts — retrying them just burns the deadline twice.
|
|
324
|
+
if (attempt.timedOut) {
|
|
325
|
+
attempt = runOnce(resolved, args, timeoutMs, env, cwd);
|
|
326
|
+
attempts += 1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const verdict = classify(resolved, attempt, timeoutMs, attempts);
|
|
330
|
+
return {
|
|
331
|
+
...base,
|
|
332
|
+
status: verdict.status,
|
|
333
|
+
path: resolved,
|
|
334
|
+
exit_code: attempt.status,
|
|
335
|
+
signal: attempt.signal,
|
|
336
|
+
attempts,
|
|
337
|
+
diagnostic: verdict.diagnostic,
|
|
338
|
+
fix: verdict.status === 'missing' || verdict.status === 'broken' ? fix : null,
|
|
339
|
+
};
|
|
340
|
+
} catch (err) {
|
|
341
|
+
// The never-throw guarantee. An invalid arg, a RangeError deadline, a
|
|
342
|
+
// filesystem that lies — all land here as this descriptor's `error`.
|
|
343
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
344
|
+
return {
|
|
345
|
+
...base,
|
|
346
|
+
status: 'error',
|
|
347
|
+
timeout_ms: timeoutMs,
|
|
348
|
+
diagnostic: `probe raised unexpectedly: ${message}`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Probe many descriptors, isolating failures: one hostile descriptor becomes
|
|
355
|
+
* its own `error` row and every other backend is still reported.
|
|
356
|
+
*/
|
|
357
|
+
export function probeTools(descriptors: readonly ToolProbeDescriptor[]): ToolProbeResult[] {
|
|
358
|
+
return descriptors.map((descriptor) => probeTool(descriptor));
|
|
359
|
+
}
|
|
@@ -159,9 +159,15 @@ export function scan(d: string): [Row[], Row[], string[]] {
|
|
|
159
159
|
for (const r of num) {
|
|
160
160
|
const s = r.supersedes ?? '—';
|
|
161
161
|
if (s && s !== '—') {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
// Comma-separated refs; a parenthesized annotation per ref marks
|
|
163
|
+
// partial supersession (house precedent: ADR-098's superseded_by).
|
|
164
|
+
for (const ref of s.split(',')) {
|
|
165
|
+
const bare = ref.replace(/\(.*?\)/g, '').trim();
|
|
166
|
+
if (!bare) continue;
|
|
167
|
+
const t = _zfill3(_lstripZeros(bare.replaceAll('ADR-', '')));
|
|
168
|
+
if (!nums.has(t)) {
|
|
169
|
+
errs.push(`${r.path}: supersedes ADR-${t} not found`);
|
|
170
|
+
}
|
|
165
171
|
}
|
|
166
172
|
}
|
|
167
173
|
}
|
|
@@ -179,21 +185,90 @@ export function row(r: Row): string {
|
|
|
179
185
|
);
|
|
180
186
|
}
|
|
181
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Decision records that live outside the ADR directory.
|
|
190
|
+
*
|
|
191
|
+
* Six files under `agents/settings/contexts/` are titled `# ADR — …`, carry a
|
|
192
|
+
* decision status, and had **zero** presence in this index — ADRs by every
|
|
193
|
+
* property except location. A decision nobody can find is a trap: later work
|
|
194
|
+
* contradicts it without knowing it existed, which is the failure ADR-124 paid
|
|
195
|
+
* for with a hand-built 44-entry archaeology sweep.
|
|
196
|
+
*
|
|
197
|
+
* Listing beats relocating, deliberately. Moving them would mean renumbering six
|
|
198
|
+
* records and rewriting ~15 inbound references, ten of them inside frozen
|
|
199
|
+
* archived roadmaps whose paths were correct when written — rewriting history to
|
|
200
|
+
* fix an index. The harm is findability, not location, so the index reaches out
|
|
201
|
+
* instead. A scan also keeps working for the next file filed in the wrong place,
|
|
202
|
+
* which a one-time move would not.
|
|
203
|
+
*/
|
|
204
|
+
const OUTSIDE_ROOTS = ['agents/settings/contexts'];
|
|
205
|
+
const DECISION_TITLE = /^#\s*ADR\s*[—–-]\s*(.+)$/m;
|
|
206
|
+
|
|
207
|
+
export interface OutsideRow {
|
|
208
|
+
path: string;
|
|
209
|
+
title: string;
|
|
210
|
+
status: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function scanOutside(repoRoot: string, roots: readonly string[] = OUTSIDE_ROOTS): OutsideRow[] {
|
|
214
|
+
const out: OutsideRow[] = [];
|
|
215
|
+
for (const rel of roots) {
|
|
216
|
+
const dir = _pyPathJoin(repoRoot, rel);
|
|
217
|
+
let names: string[];
|
|
218
|
+
try {
|
|
219
|
+
names = fs.readdirSync(dir);
|
|
220
|
+
} catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
for (const name of names.slice().sort()) {
|
|
224
|
+
if (!name.endsWith('.md')) continue;
|
|
225
|
+
const text = fs.readFileSync(_pyPathJoin(dir, name), 'utf-8');
|
|
226
|
+
const m = DECISION_TITLE.exec(text);
|
|
227
|
+
if (!m) continue;
|
|
228
|
+
const status =
|
|
229
|
+
/^\s*>?\s*\*\*Status:\*\*\s*(.+)$/m.exec(text)?.[1] ??
|
|
230
|
+
/^status:\s*(.+)$/m.exec(text)?.[1] ??
|
|
231
|
+
'—';
|
|
232
|
+
out.push({
|
|
233
|
+
path: `${rel}/${name}`,
|
|
234
|
+
title: (m[1] ?? name).trim(),
|
|
235
|
+
status: status.trim().replace(/\s*·.*$/, ''),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
182
242
|
/** Mirror `render(num, leg)`. */
|
|
183
|
-
export function render(num: Row[], leg: Row[]): string {
|
|
243
|
+
export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): string {
|
|
184
244
|
const out = [
|
|
185
245
|
'# ADR Index',
|
|
186
246
|
'',
|
|
187
247
|
'_Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._',
|
|
188
248
|
'',
|
|
189
249
|
];
|
|
190
|
-
if (num.length === 0 && leg.length === 0) {
|
|
250
|
+
if (num.length === 0 && leg.length === 0 && outside.length === 0) {
|
|
191
251
|
return [...out, 'No ADRs yet.', ''].join('\n');
|
|
192
252
|
}
|
|
193
253
|
out.push(HEAD, ...num.map(row));
|
|
194
254
|
if (leg.length > 0) {
|
|
195
255
|
out.push('', '## Unnumbered (legacy)', '', HEAD, ...leg.map(row));
|
|
196
256
|
}
|
|
257
|
+
if (outside.length > 0) {
|
|
258
|
+
out.push(
|
|
259
|
+
'',
|
|
260
|
+
'## Decision records outside this directory',
|
|
261
|
+
'',
|
|
262
|
+
'Scanned, not hand-listed. These files are titled `# ADR — …` and record a',
|
|
263
|
+
'decision, but live elsewhere in the tree. They are indexed here so a decision',
|
|
264
|
+
'cannot be unfindable merely because of where it was filed; their paths are',
|
|
265
|
+
'unchanged and their inbound references still resolve.',
|
|
266
|
+
'',
|
|
267
|
+
'| File | Title | Status |',
|
|
268
|
+
'|---|---|---|',
|
|
269
|
+
...outside.map((o) => `| \`${o.path}\` | ${o.title} | ${o.status} |`),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
197
272
|
return [...out, ''].join('\n');
|
|
198
273
|
}
|
|
199
274
|
|
|
@@ -229,7 +304,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
229
304
|
if (errs.length > 0) {
|
|
230
305
|
return 2;
|
|
231
306
|
}
|
|
232
|
-
const rendered = render(num, leg);
|
|
307
|
+
const rendered = render(num, leg, scanOutside(process.cwd()));
|
|
233
308
|
const idx = _pyPathJoin(d, 'INDEX.md');
|
|
234
309
|
if (check) {
|
|
235
310
|
let cur = '';
|
|
@@ -57,8 +57,34 @@ export const AREAS: Record<string, AreaMeta> = {
|
|
|
57
57
|
contract: 'smoke-contracts.md',
|
|
58
58
|
scope: 'Per-tier smoke contracts, baseline locks, regression gates.',
|
|
59
59
|
},
|
|
60
|
+
memory: {
|
|
61
|
+
contract: 'memory-visibility-v1.md',
|
|
62
|
+
scope: 'Memory / knowledge surfaces. RETIRED area — its single ADR is superseded by the agent-memory-layer removal; listed so the directory is audited rather than invisible.',
|
|
63
|
+
},
|
|
60
64
|
};
|
|
61
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Area directories present on disk but absent from `AREAS`.
|
|
68
|
+
*
|
|
69
|
+
* `docs/adrs/memory/` sat on disk unlisted, and therefore unaudited, for an
|
|
70
|
+
* unknown period: the audit iterates a hardcoded inventory and had no way to
|
|
71
|
+
* notice a directory nobody told it about. Adding `memory` fixes the instance —
|
|
72
|
+
* this fixes the class. A coverage audit blind to a whole area is the same defect
|
|
73
|
+
* shape as a gate watching a path that no longer exists.
|
|
74
|
+
*/
|
|
75
|
+
export function unlistedAreaDirs(adrsRoot: string): string[] {
|
|
76
|
+
try {
|
|
77
|
+
return fs
|
|
78
|
+
.readdirSync(adrsRoot, { withFileTypes: true })
|
|
79
|
+
.filter((e) => e.isDirectory())
|
|
80
|
+
.map((e) => e.name)
|
|
81
|
+
.filter((n) => !(n in AREAS))
|
|
82
|
+
.sort();
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
// ^(\d{4})-([a-z0-9-]+)\.md$
|
|
63
89
|
const NAMED = /^(\d{4})-([a-z0-9-]+)\.md$/;
|
|
64
90
|
// ^---\n(.*?)\n--- (DOTALL)
|
|
@@ -252,6 +278,13 @@ export function cmd_report(): number {
|
|
|
252
278
|
export function cmd_check(): number {
|
|
253
279
|
let hard = 0;
|
|
254
280
|
let warn = 0;
|
|
281
|
+
for (const dir of unlistedAreaDirs(ADR_ROOT)) {
|
|
282
|
+
process.stderr.write(
|
|
283
|
+
`❌ ${dir}/: area directory exists on disk but is absent from AREAS — it is not audited at all. ` +
|
|
284
|
+
`Add it to the inventory (or delete the directory).\n`,
|
|
285
|
+
);
|
|
286
|
+
hard += 1;
|
|
287
|
+
}
|
|
255
288
|
for (const [area, meta] of Object.entries(AREAS)) {
|
|
256
289
|
const [adrs, errs] = scan_area(area);
|
|
257
290
|
for (const e of errs) {
|