@event4u/agent-config 9.27.0 → 9.28.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/AGENTS.md +12 -11
- package/CHANGELOG.md +99 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
- package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +29 -5
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +3 -1
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +6 -10
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-2BE5I2MN.js} +8 -8
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-PQSAQKHW.js} +466 -304
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +7 -7
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +5 -5
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +5 -5
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +272 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
- package/dist/cli-delegate/cmd_settings_set.js +7 -2967
- package/dist/cli-delegate/cmd_sync.js +5 -5
- package/dist/cli-delegate/cmd_uninstall.js +5 -5
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +5 -5
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +100 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +9004 -6132
- package/dist/install/install.mjs +697 -507
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +127 -20
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +80 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +29 -8
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +6 -10
- package/src/config/budgets.yml +29 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +82 -16
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +84 -1
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +342 -0
- package/src/scripts/_lib/self_repair_store.ts +100 -0
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +126 -7
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +77 -6
- package/src/scripts/self_repair_cli.ts +240 -0
- package/src/scripts/self_repair_hook.ts +241 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-config mcp:available` — what is declared, what can actually launch,
|
|
3
|
+
* and the static allowlist that is neither.
|
|
4
|
+
*
|
|
5
|
+
* Phase 2.3 of `road-to-capability-answerability`. Three surfaces in this tree
|
|
6
|
+
* answer "which tools do I have" and they disagree: `mcp.json` declares the MCP
|
|
7
|
+
* servers, `TOOL_REGISTRY` is a two-entry constant governing what a skill may
|
|
8
|
+
* put in `allowed_tools`, and the `mcp` skill tabulates a larger set as prose.
|
|
9
|
+
* An agent asking the question reads whichever it finds first. This verb prints
|
|
10
|
+
* all three, labelled, in one place — the step's requirement is literally "keep
|
|
11
|
+
* the two apart in the output; conflating them is the current defect".
|
|
12
|
+
*
|
|
13
|
+
* ## The honesty constraint, which shapes the whole design
|
|
14
|
+
*
|
|
15
|
+
* This verb does NOT perform an MCP handshake. It reports whether each declared
|
|
16
|
+
* server's command resolves to an executable on `PATH`, which is a strictly
|
|
17
|
+
* weaker fact, so it is labelled `launchable` and never `reachable`. Calling a
|
|
18
|
+
* PATH lookup "reachable" would repeat verbatim the defect Phase 1.2 exists to
|
|
19
|
+
* fix — a probe reporting a derived answer as though it were a detection.
|
|
20
|
+
*
|
|
21
|
+
* Remote (`url`) servers are reported as declared-but-unprobed rather than
|
|
22
|
+
* fetched. A read-only status verb that opens outbound connections would add an
|
|
23
|
+
* egress leg to a surface that has none (`lethal-trifecta-guard`), and a probe
|
|
24
|
+
* is not worth a network call the caller did not ask for.
|
|
25
|
+
*
|
|
26
|
+
* Read-only by construction: no writer is imported and no file is opened for
|
|
27
|
+
* writing.
|
|
28
|
+
*
|
|
29
|
+
* Exit codes: `0` answered, including "nothing declared" · `1` `mcp.json` exists
|
|
30
|
+
* but cannot be parsed — a declaration file that does not parse is a failure to
|
|
31
|
+
* answer, not an answer of "none".
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
import process from 'node:process';
|
|
37
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
|
+
|
|
39
|
+
import { resolve_project_root } from '../_lib/agent_settings.js';
|
|
40
|
+
import { list_tools } from '../tool_registry.js';
|
|
41
|
+
|
|
42
|
+
/** How a declared server is addressed. */
|
|
43
|
+
export type ServerTransport = 'command' | 'url' | 'unknown';
|
|
44
|
+
|
|
45
|
+
/** Whether the declared command could be started at all. */
|
|
46
|
+
export type LaunchState = 'launchable' | 'not-on-path' | 'not-probed';
|
|
47
|
+
|
|
48
|
+
export interface DeclaredServer {
|
|
49
|
+
name: string;
|
|
50
|
+
transport: ServerTransport;
|
|
51
|
+
/** The command word, or the URL, or null when the entry declares neither. */
|
|
52
|
+
target: string | null;
|
|
53
|
+
launch: LaunchState;
|
|
54
|
+
/** Absolute path the command resolved to, when it resolved. */
|
|
55
|
+
resolved: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface McpAvailableOptions {
|
|
59
|
+
cwd: string;
|
|
60
|
+
json: boolean;
|
|
61
|
+
/** `PATH` to search. Injected by the tests; defaults to the process value. */
|
|
62
|
+
pathEnv: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface McpAvailableResult {
|
|
66
|
+
code: 0 | 1;
|
|
67
|
+
out: string[];
|
|
68
|
+
err: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface RawServerEntry {
|
|
72
|
+
command?: unknown;
|
|
73
|
+
url?: unknown;
|
|
74
|
+
args?: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The first executable named `command` on `pathEnv`, or `null`.
|
|
79
|
+
*
|
|
80
|
+
* A command containing a path separator is treated as a path and checked
|
|
81
|
+
* directly — that is how `mcp.json` entries pointing at a repo-local script
|
|
82
|
+
* behave, and searching `PATH` for `./bin/server` would always miss.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveOnPath(command: string, pathEnv: string, cwd: string): string | null {
|
|
85
|
+
if (command === '') return null;
|
|
86
|
+
const candidates = command.includes(path.sep)
|
|
87
|
+
? [path.resolve(cwd, command)]
|
|
88
|
+
: pathEnv.split(path.delimiter).filter((p) => p !== '').map((dir) => path.join(dir, command));
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
try {
|
|
91
|
+
const stat = fs.statSync(candidate);
|
|
92
|
+
if (stat.isFile() && (stat.mode & 0o111) !== 0) return candidate;
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Parse one `servers` entry into the shape the report prints. */
|
|
101
|
+
export function classifyServer(
|
|
102
|
+
name: string,
|
|
103
|
+
entry: RawServerEntry,
|
|
104
|
+
pathEnv: string,
|
|
105
|
+
cwd: string,
|
|
106
|
+
): DeclaredServer {
|
|
107
|
+
if (typeof entry.command === 'string' && entry.command !== '') {
|
|
108
|
+
const resolved = resolveOnPath(entry.command, pathEnv, cwd);
|
|
109
|
+
return {
|
|
110
|
+
name,
|
|
111
|
+
transport: 'command',
|
|
112
|
+
target: entry.command,
|
|
113
|
+
launch: resolved === null ? 'not-on-path' : 'launchable',
|
|
114
|
+
resolved,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (typeof entry.url === 'string' && entry.url !== '') {
|
|
118
|
+
return { name, transport: 'url', target: entry.url, launch: 'not-probed', resolved: null };
|
|
119
|
+
}
|
|
120
|
+
return { name, transport: 'unknown', target: null, launch: 'not-probed', resolved: null };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function runMcpAvailable(opts: McpAvailableOptions): McpAvailableResult {
|
|
124
|
+
const out: string[] = [];
|
|
125
|
+
const err: string[] = [];
|
|
126
|
+
|
|
127
|
+
const [projectRoot] = resolve_project_root(null, { cwd: opts.cwd });
|
|
128
|
+
const configPath = path.join(projectRoot, 'mcp.json');
|
|
129
|
+
|
|
130
|
+
let declared: DeclaredServer[] = [];
|
|
131
|
+
let configExists = true;
|
|
132
|
+
let rawText: string;
|
|
133
|
+
try {
|
|
134
|
+
rawText = fs.readFileSync(configPath, 'utf-8');
|
|
135
|
+
} catch {
|
|
136
|
+
configExists = false;
|
|
137
|
+
rawText = '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (configExists) {
|
|
141
|
+
let parsed: unknown;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(rawText);
|
|
144
|
+
} catch (exc) {
|
|
145
|
+
err.push(
|
|
146
|
+
`❌ mcp:available — ${configPath} exists but is not valid JSON.`,
|
|
147
|
+
` ${exc instanceof Error ? exc.message : String(exc)}`,
|
|
148
|
+
' Refusing to report "no servers": an unparseable declaration file is a',
|
|
149
|
+
' failure to answer, not an answer.',
|
|
150
|
+
);
|
|
151
|
+
return { code: 1, out, err };
|
|
152
|
+
}
|
|
153
|
+
const servers =
|
|
154
|
+
typeof parsed === 'object' && parsed !== null
|
|
155
|
+
? ((parsed as { servers?: unknown }).servers ?? {})
|
|
156
|
+
: {};
|
|
157
|
+
if (typeof servers === 'object' && servers !== null && !Array.isArray(servers)) {
|
|
158
|
+
declared = Object.keys(servers)
|
|
159
|
+
.sort()
|
|
160
|
+
.map((name) =>
|
|
161
|
+
classifyServer(
|
|
162
|
+
name,
|
|
163
|
+
(servers as Record<string, RawServerEntry>)[name] ?? {},
|
|
164
|
+
opts.pathEnv,
|
|
165
|
+
projectRoot,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const registryTools = list_tools().map((t) => t.name).sort();
|
|
172
|
+
|
|
173
|
+
if (opts.json) {
|
|
174
|
+
out.push(
|
|
175
|
+
JSON.stringify(
|
|
176
|
+
{
|
|
177
|
+
config: configExists ? configPath : null,
|
|
178
|
+
declared_servers: declared,
|
|
179
|
+
tool_registry: registryTools,
|
|
180
|
+
handshake_performed: false,
|
|
181
|
+
},
|
|
182
|
+
null,
|
|
183
|
+
2,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
return { code: 0, out, err };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
out.push(`declared servers ${configExists ? configPath : '— no mcp.json in this project'}`);
|
|
190
|
+
if (declared.length === 0) {
|
|
191
|
+
out.push(' none');
|
|
192
|
+
}
|
|
193
|
+
for (const s of declared) {
|
|
194
|
+
const state =
|
|
195
|
+
s.launch === 'launchable'
|
|
196
|
+
? `launchable — ${s.resolved ?? ''}`
|
|
197
|
+
: s.launch === 'not-on-path'
|
|
198
|
+
? `NOT on PATH — \`${s.target ?? ''}\` does not resolve to an executable`
|
|
199
|
+
: s.transport === 'url'
|
|
200
|
+
? 'remote — declared, deliberately not probed'
|
|
201
|
+
: 'malformed entry — declares neither command nor url';
|
|
202
|
+
out.push(` ${s.name} ${state}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
out.push('');
|
|
206
|
+
out.push('tool registry static allowlist for skill `allowed_tools` — NOT MCP, not probed');
|
|
207
|
+
out.push(` ${registryTools.length > 0 ? registryTools.join(', ') : 'none'}`);
|
|
208
|
+
|
|
209
|
+
out.push('');
|
|
210
|
+
out.push('⚠️ No MCP handshake was performed. "launchable" means the command resolves to an');
|
|
211
|
+
out.push(' executable on PATH — strictly weaker than "the server responds". Any list of');
|
|
212
|
+
out.push(' TOOLS a server exposes can only come from a live session, never from here.');
|
|
213
|
+
|
|
214
|
+
return { code: 0, out, err };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface ParsedArgv {
|
|
218
|
+
ok: boolean;
|
|
219
|
+
message?: string;
|
|
220
|
+
json?: boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
224
|
+
let json = false;
|
|
225
|
+
for (const a of argv) {
|
|
226
|
+
if (a === '--json') {
|
|
227
|
+
json = true;
|
|
228
|
+
} else if (a === '-h' || a === '--help') {
|
|
229
|
+
return { ok: false, message: 'usage: agent-config mcp:available [--json]' };
|
|
230
|
+
} else {
|
|
231
|
+
return { ok: false, message: `unknown argument: ${a}` };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, json };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
238
|
+
const parsed = parseArgv(argv);
|
|
239
|
+
if (!parsed.ok) {
|
|
240
|
+
process.stderr.write(`${parsed.message ?? 'usage error'}\n`);
|
|
241
|
+
return 2;
|
|
242
|
+
}
|
|
243
|
+
const result = runMcpAvailable({
|
|
244
|
+
cwd: process.cwd(),
|
|
245
|
+
json: parsed.json === true,
|
|
246
|
+
pathEnv: process.env['PATH'] ?? '',
|
|
247
|
+
});
|
|
248
|
+
for (const line of result.out) process.stdout.write(`${line}\n`);
|
|
249
|
+
for (const line of result.err) process.stderr.write(`${line}\n`);
|
|
250
|
+
return result.code;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _isCliEntry(): boolean {
|
|
254
|
+
if (process.argv[1] === undefined) return false;
|
|
255
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
256
|
+
if (import.meta.url === argvUrl) return true;
|
|
257
|
+
try {
|
|
258
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (_isCliEntry()) {
|
|
265
|
+
process.exitCode = main();
|
|
266
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-config packs:active` — which packs are active HERE, and from which file.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 of `road-to-capability-answerability`. The sibling verb `packs ls`
|
|
5
|
+
* lists the packs the *catalogue* knows about, which reads authoritative and
|
|
6
|
+
* answers a different question. Five safety floors open with "auto-activates
|
|
7
|
+
* when pack X is installed" and, before this verb, nothing could tell an agent
|
|
8
|
+
* whether pack X is installed in the project it is standing in — so the honest
|
|
9
|
+
* answer was an inference from a filename, which is the whole defect class this
|
|
10
|
+
* roadmap exists to close.
|
|
11
|
+
*
|
|
12
|
+
* The answer has three parts and all three are printed, because two of them are
|
|
13
|
+
* what makes the third checkable:
|
|
14
|
+
*
|
|
15
|
+
* - the resolved **profile id** and the layer that resolved it,
|
|
16
|
+
* - the **packs** that profile declares, and the file they were read from,
|
|
17
|
+
* - whether the resolution took the **degraded branch**.
|
|
18
|
+
*
|
|
19
|
+
* That last part is the reason this is a probe rather than a one-line lookup.
|
|
20
|
+
* `resolve_profile` has a branch — a settings file exists but declares no
|
|
21
|
+
* `profile.id` — that returns the default id with an EMPTY body: no packs, no
|
|
22
|
+
* personas. An agent asking "which profile is active" gets `developer` and
|
|
23
|
+
* concludes the developer packs are on. They are not, and nothing says so. The
|
|
24
|
+
* carve-out module records this as one of nine keys where absent is not the
|
|
25
|
+
* template default (`src/shared/settingsCarveOut.ts`, `profile.id`); this verb
|
|
26
|
+
* is where it becomes visible at the moment somebody asks.
|
|
27
|
+
*
|
|
28
|
+
* Read-only by construction: nothing here opens a file for writing.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: `0` answered (including the degraded answer, which is a real
|
|
31
|
+
* answer) · `1` the profile id names a file that does not exist.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
import process from 'node:process';
|
|
37
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_PROJECT_FILE,
|
|
41
|
+
load_agent_settings,
|
|
42
|
+
resolve_project_root,
|
|
43
|
+
} from '../_lib/agent_settings.js';
|
|
44
|
+
import {
|
|
45
|
+
ProfileError,
|
|
46
|
+
profile_file,
|
|
47
|
+
resolve_profile,
|
|
48
|
+
SOURCE_MISSING,
|
|
49
|
+
type ResolvedProfile,
|
|
50
|
+
} from '../config/profiles.js';
|
|
51
|
+
|
|
52
|
+
export interface PacksActiveOptions {
|
|
53
|
+
/** Where to resolve from. Defaults to the process CWD. */
|
|
54
|
+
cwd: string;
|
|
55
|
+
json: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PacksActiveResult {
|
|
59
|
+
code: 0 | 1;
|
|
60
|
+
out: string[];
|
|
61
|
+
err: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Human-readable gloss per `resolve_profile` source constant.
|
|
66
|
+
*
|
|
67
|
+
* The raw constants (`user-settings`, `missing`, …) are precise and mean
|
|
68
|
+
* nothing to a reader who has not read `profiles.ts`. A probe whose output
|
|
69
|
+
* needs its own source lookup has not answered the question.
|
|
70
|
+
*/
|
|
71
|
+
const SOURCE_GLOSS: Readonly<Record<string, string>> = {
|
|
72
|
+
runtime: 'set for this run (runtime override)',
|
|
73
|
+
env: 'AGENT_CONFIG_PROFILE_ID environment variable',
|
|
74
|
+
'user-settings': `profile.id in ${DEFAULT_PROJECT_FILE}`,
|
|
75
|
+
pack: 'declared by an installed pack',
|
|
76
|
+
default: 'no settings file — package default',
|
|
77
|
+
[SOURCE_MISSING]: `settings file present but it declares no profile.id`,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The file the profile body was read from, or `null` when it was never read.
|
|
82
|
+
*
|
|
83
|
+
* `null` is the degraded branch and only that branch: `resolve_profile`
|
|
84
|
+
* returns an id without touching a file, so there is no path to name. Every
|
|
85
|
+
* other branch delegates to the resolver's own `profile_file`, which is why
|
|
86
|
+
* this function is four lines and not a second path search.
|
|
87
|
+
*/
|
|
88
|
+
export function profileFileFor(projectRoot: string, profile: ResolvedProfile): string | null {
|
|
89
|
+
if (profile.source === SOURCE_MISSING) return null;
|
|
90
|
+
const resolved = profile_file(projectRoot, profile.id);
|
|
91
|
+
return _isFile(resolved) ? resolved : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _isFile(p: string): boolean {
|
|
95
|
+
try {
|
|
96
|
+
return fs.statSync(p).isFile();
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function runPacksActive(opts: PacksActiveOptions): PacksActiveResult {
|
|
103
|
+
const out: string[] = [];
|
|
104
|
+
const err: string[] = [];
|
|
105
|
+
|
|
106
|
+
const [projectRoot] = resolve_project_root(null, { cwd: opts.cwd });
|
|
107
|
+
const settings = load_agent_settings({ cwd: opts.cwd });
|
|
108
|
+
|
|
109
|
+
let profile: ResolvedProfile;
|
|
110
|
+
try {
|
|
111
|
+
profile = resolve_profile({ project_root: projectRoot, user_settings: settings });
|
|
112
|
+
} catch (exc) {
|
|
113
|
+
if (exc instanceof ProfileError) {
|
|
114
|
+
err.push(
|
|
115
|
+
`❌ packs:active — ${exc.message}`,
|
|
116
|
+
' The profile id resolves but its file does not exist, so no pack set can be',
|
|
117
|
+
' named. This is a broken install or a hand-edited profile.id, not an empty one.',
|
|
118
|
+
);
|
|
119
|
+
return { code: 1, out, err };
|
|
120
|
+
}
|
|
121
|
+
throw exc;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const degraded = profile.source === SOURCE_MISSING;
|
|
125
|
+
const file = profileFileFor(projectRoot, profile);
|
|
126
|
+
const gloss = SOURCE_GLOSS[profile.source] ?? profile.source;
|
|
127
|
+
|
|
128
|
+
if (opts.json) {
|
|
129
|
+
out.push(
|
|
130
|
+
JSON.stringify(
|
|
131
|
+
{
|
|
132
|
+
profile_id: profile.id,
|
|
133
|
+
source: profile.source,
|
|
134
|
+
degraded,
|
|
135
|
+
packs: [...profile.packs],
|
|
136
|
+
personas: [...profile.personas],
|
|
137
|
+
profile_file: file,
|
|
138
|
+
project_root: projectRoot,
|
|
139
|
+
},
|
|
140
|
+
null,
|
|
141
|
+
2,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
return { code: 0, out, err };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
out.push(`profile ${profile.id} (${gloss})`);
|
|
148
|
+
out.push(
|
|
149
|
+
profile.packs.length > 0
|
|
150
|
+
? `packs ${profile.packs.join(', ')} (${String(profile.packs.length)})`
|
|
151
|
+
: 'packs none',
|
|
152
|
+
);
|
|
153
|
+
out.push(`file ${file ?? '— the profile body was never loaded'}`);
|
|
154
|
+
out.push(`root ${projectRoot}`);
|
|
155
|
+
|
|
156
|
+
if (degraded) {
|
|
157
|
+
out.push(
|
|
158
|
+
'',
|
|
159
|
+
'⚠️ Degraded resolution — this is the answer, not a warning about it.',
|
|
160
|
+
` A ${DEFAULT_PROJECT_FILE} exists here but declares no profile.id, so the id fell`,
|
|
161
|
+
' back to the package default while the profile BODY was never read: zero packs,',
|
|
162
|
+
' zero personas. Any rule that says "auto-activates when pack X is installed"',
|
|
163
|
+
' cannot activate in this project, and nothing else reports that.',
|
|
164
|
+
' Fix: set profile.id in the settings file, or run `agent-config use <id>`.',
|
|
165
|
+
);
|
|
166
|
+
} else if (profile.packs.length === 0) {
|
|
167
|
+
out.push(
|
|
168
|
+
'',
|
|
169
|
+
`ℹ️ The ${profile.id} profile declares no packs. Pack-gated rules stay inert here.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (profile.warning !== null && !degraded) {
|
|
174
|
+
out.push('', `⚠️ ${profile.warning}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { code: 0, out, err };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface ParsedArgv {
|
|
181
|
+
ok: boolean;
|
|
182
|
+
message?: string;
|
|
183
|
+
json?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
187
|
+
let json = false;
|
|
188
|
+
for (const a of argv) {
|
|
189
|
+
if (a === '--json') {
|
|
190
|
+
json = true;
|
|
191
|
+
} else if (a === '-h' || a === '--help') {
|
|
192
|
+
return { ok: false, message: 'usage: agent-config packs:active [--json]' };
|
|
193
|
+
} else {
|
|
194
|
+
return { ok: false, message: `unknown argument: ${a}` };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, json };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
201
|
+
const parsed = parseArgv(argv);
|
|
202
|
+
if (!parsed.ok) {
|
|
203
|
+
process.stderr.write(`${parsed.message ?? 'usage error'}\n`);
|
|
204
|
+
return 2;
|
|
205
|
+
}
|
|
206
|
+
const result = runPacksActive({ cwd: process.cwd(), json: parsed.json === true });
|
|
207
|
+
for (const line of result.out) process.stdout.write(`${line}\n`);
|
|
208
|
+
for (const line of result.err) process.stderr.write(`${line}\n`);
|
|
209
|
+
return result.code;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function _isCliEntry(): boolean {
|
|
213
|
+
if (process.argv[1] === undefined) return false;
|
|
214
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
215
|
+
if (import.meta.url === argvUrl) return true;
|
|
216
|
+
try {
|
|
217
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (_isCliEntry()) {
|
|
224
|
+
process.exitCode = main();
|
|
225
|
+
}
|