@ghl-ai/aw 0.1.44-beta.1 → 0.1.44-beta.2
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/c4/claudePluginRegistry.mjs +142 -0
- package/c4/codexConfig.mjs +148 -0
- package/c4/codexPromptInjector.mjs +187 -0
- package/c4/commandSurface.mjs +286 -0
- package/c4/detect.mjs +62 -0
- package/c4/diagnostics.mjs +536 -0
- package/c4/eccRegistryBridge.mjs +184 -0
- package/c4/ghCli.mjs +94 -0
- package/c4/gitAuth.mjs +384 -0
- package/c4/index.mjs +64 -0
- package/c4/jsonMerge.mjs +229 -0
- package/c4/mcpServer.mjs +160 -0
- package/c4/mcpSmokeProbe.mjs +201 -0
- package/c4/preflight.mjs +254 -0
- package/c4/repoLocalClaudeSettings.mjs +54 -0
- package/c4/repoLocalIgnore.mjs +157 -0
- package/c4/repoRootInstructions.mjs +166 -0
- package/c4/secrets.mjs +55 -0
- package/c4/slimRouter.mjs +472 -0
- package/cli.mjs +7 -0
- package/commands/c4.mjs +387 -0
- package/ecc.mjs +7 -72
- package/integrate.mjs +6 -6
- package/mcp.mjs +0 -1
- package/package.json +5 -3
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/diagnostics.mjs — end-of-run health probes + summary formatting.
|
|
3
|
+
*
|
|
4
|
+
* Five exports, each with a separate, tightly-scoped responsibility:
|
|
5
|
+
*
|
|
6
|
+
* summarizeAsOneLine(state)
|
|
7
|
+
* Pure formatter. Renders a single ANSI-clean line for stdout that
|
|
8
|
+
* captures the install verdict at a glance.
|
|
9
|
+
*
|
|
10
|
+
* diagnoseAwRouterView({ awHome })
|
|
11
|
+
* Codex-relevant: replays the upstream `find * /skills/* /SKILL.md`
|
|
12
|
+
* enumeration logic against ~/.aw/.aw_registry/platform/core/skills/
|
|
13
|
+
* and reports which expected stage skills are visible to the
|
|
14
|
+
* SessionStart hook. Catches Bug A failures (registry path empty
|
|
15
|
+
* post-init).
|
|
16
|
+
*
|
|
17
|
+
* diagnosePromptRouterInjection(home, opts)
|
|
18
|
+
* Codex-relevant: spawnSyncs the wrapper at <home>/.codex/hooks/
|
|
19
|
+
* aw-user-prompt-submit.sh with synthetic stdin (steady-state OR
|
|
20
|
+
* first-turn shape per opts.simulateFirstTurn) and asserts the
|
|
21
|
+
* stdout JSON envelope contains <EXTREMELY_IMPORTANT> framing.
|
|
22
|
+
* Tests both that the wrapper is registered AND that it actually
|
|
23
|
+
* emits the router (catches "wrapper installed but does nothing").
|
|
24
|
+
*
|
|
25
|
+
* diagnoseSkillResolution({ harness, home, awHome, eccHome, skills })
|
|
26
|
+
* Multi-path × multi-skill probe. For each skill, checks four
|
|
27
|
+
* candidate paths in priority order. Top-level ok=true ONLY if
|
|
28
|
+
* using-aw-skills AND every stage skill the slim card promises
|
|
29
|
+
* resolve via at least one path. Catches "router fires but
|
|
30
|
+
* dispatches to ENOENT" silent failures.
|
|
31
|
+
*
|
|
32
|
+
* dumpPostInitState({ harness, cwd, awHome, configPaths, writer, shell })
|
|
33
|
+
* Triage dump to stderr (NEVER stdout) — token-redacted gh auth
|
|
34
|
+
* status, git remote state, key-only enumeration of config files
|
|
35
|
+
* (no values, since some keys carry secrets), and the first 10
|
|
36
|
+
* SKILL.md paths visible to the registry enumerator.
|
|
37
|
+
*
|
|
38
|
+
* All probes are pure or replay-style; none mutate state. Adapters
|
|
39
|
+
* (writer, shell, spawnSync) are dependency-injected for testability.
|
|
40
|
+
*
|
|
41
|
+
* Contract: spec.md::§"c4/diagnostics.mjs", tasks.md::4.2.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
existsSync,
|
|
46
|
+
readdirSync,
|
|
47
|
+
readFileSync,
|
|
48
|
+
statSync,
|
|
49
|
+
} from 'node:fs';
|
|
50
|
+
import { join } from 'node:path';
|
|
51
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {'cursor-cloud' | 'codex-web' | 'claude-web' | 'unknown'} Harness
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
58
|
+
* Stable expected sets — referenced by multiple probes.
|
|
59
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
60
|
+
|
|
61
|
+
const EXPECTED_STAGE_SKILLS = Object.freeze([
|
|
62
|
+
'aw-plan',
|
|
63
|
+
'aw-build',
|
|
64
|
+
'aw-investigate',
|
|
65
|
+
'aw-review',
|
|
66
|
+
]);
|
|
67
|
+
const ROUTER_SKILL = 'using-aw-skills';
|
|
68
|
+
const DEFAULT_SKILL_LIST = Object.freeze([ROUTER_SKILL, ...EXPECTED_STAGE_SKILLS]);
|
|
69
|
+
|
|
70
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
71
|
+
* summarizeAsOneLine
|
|
72
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
73
|
+
|
|
74
|
+
const CHECK = '\u2713'; // ✓
|
|
75
|
+
const CROSS = '\u2717'; // ✗
|
|
76
|
+
const DOT = ' \u00B7 '; // " · "
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {object} state
|
|
80
|
+
* Combination of PreflightResult + per-harness install outcomes:
|
|
81
|
+
* - state.harness, state.hasToken, state.tokenSource, state.authOk
|
|
82
|
+
* - state.bridge?: { ok: boolean } (codex-web only)
|
|
83
|
+
* - state.injector?: { ok: boolean } (codex-web only)
|
|
84
|
+
* - state.mcpProbe?: 'ok' | 'invalid-token' | 'unauthorized' | 'unreachable' | 'unknown'
|
|
85
|
+
* - state.didInit: boolean
|
|
86
|
+
* - state.durationMs: number
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function summarizeAsOneLine(state) {
|
|
90
|
+
if (!state || typeof state !== 'object') {
|
|
91
|
+
throw new Error('summarizeAsOneLine: state object required');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!state.hasToken) {
|
|
95
|
+
return `[aw-c4] ${state.harness}${DOT}no token${DOT}skipped (agent continues)`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parts = [`[aw-c4] ${state.harness}`];
|
|
99
|
+
// authOk === null means "not probed" (diagnose mode skips the auth probe).
|
|
100
|
+
// Render '?' in that case to avoid the misleading "GITHUB_PAT ✗" tick that
|
|
101
|
+
// would otherwise show on every diagnose run.
|
|
102
|
+
const authTick = state.authOk == null ? '?' : (state.authOk ? CHECK : CROSS);
|
|
103
|
+
parts.push(`${state.tokenSource} ${authTick}`);
|
|
104
|
+
|
|
105
|
+
if (state.harness === 'codex-web') {
|
|
106
|
+
if (state.bridge != null) parts.push(`bridge ${state.bridge.ok ? CHECK : CROSS}`);
|
|
107
|
+
if (state.injector != null) parts.push(`injector ${state.injector.ok ? CHECK : CROSS}`);
|
|
108
|
+
} else {
|
|
109
|
+
parts.push(`auth ${authTick}`);
|
|
110
|
+
parts.push('bridge n/a');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (state.mcpProbe) parts.push(`mcp ${state.mcpProbe}`);
|
|
114
|
+
|
|
115
|
+
const seconds = Number(state.durationMs ?? 0) / 1000;
|
|
116
|
+
parts.push(`init ${seconds.toFixed(1)}s`);
|
|
117
|
+
parts.push(state.didInit ? 'ready' : 'skipped');
|
|
118
|
+
|
|
119
|
+
return parts.join(DOT);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
123
|
+
* diagnoseAwRouterView
|
|
124
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Replay of the upstream session-start.sh enumerator scan:
|
|
128
|
+
* find <awHome>/.aw_registry/platform -path STAR/skills/STAR/SKILL.md
|
|
129
|
+
*
|
|
130
|
+
* @param {object} opts
|
|
131
|
+
* @param {string} opts.awHome
|
|
132
|
+
* @returns {{ enumerableSkills: string[], expected: string[], missing: string[], ok: boolean }}
|
|
133
|
+
*/
|
|
134
|
+
export function diagnoseAwRouterView({ awHome } = {}) {
|
|
135
|
+
if (!awHome) throw new Error('diagnoseAwRouterView: awHome is required');
|
|
136
|
+
|
|
137
|
+
const platformRoot = join(awHome, '.aw_registry/platform');
|
|
138
|
+
const enumerableSkills = enumerateRegistrySkills(platformRoot);
|
|
139
|
+
const expected = [...EXPECTED_STAGE_SKILLS];
|
|
140
|
+
const missing = expected.filter((s) => !enumerableSkills.includes(s));
|
|
141
|
+
const ok = missing.length === 0;
|
|
142
|
+
|
|
143
|
+
return { enumerableSkills, expected, missing, ok };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Walk <root>/<team>/skills/<skill>/SKILL.md two levels deep, mirroring the
|
|
148
|
+
* upstream find pattern. Returns the deduped sorted list of skill
|
|
149
|
+
* directory names.
|
|
150
|
+
*/
|
|
151
|
+
function enumerateRegistrySkills(platformRoot) {
|
|
152
|
+
if (!existsSync(platformRoot)) return [];
|
|
153
|
+
const set = new Set();
|
|
154
|
+
let teams;
|
|
155
|
+
try {
|
|
156
|
+
teams = readdirSync(platformRoot, { withFileTypes: true });
|
|
157
|
+
} catch {
|
|
158
|
+
// readdir on the platform root failed (EACCES, ENOENT). Treat as
|
|
159
|
+
// "no skills enumerable" — the caller's missing-list will surface it.
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
for (const team of teams) {
|
|
163
|
+
if (!team.isDirectory()) continue;
|
|
164
|
+
const skillsDir = join(platformRoot, team.name, 'skills');
|
|
165
|
+
if (!existsSync(skillsDir)) continue;
|
|
166
|
+
let skills;
|
|
167
|
+
try {
|
|
168
|
+
skills = readdirSync(skillsDir, { withFileTypes: true });
|
|
169
|
+
} catch {
|
|
170
|
+
// readdir on a team's skills/ subdirectory failed — skip this team
|
|
171
|
+
// dir but continue probing siblings.
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (const skill of skills) {
|
|
175
|
+
if (!skill.isDirectory()) continue;
|
|
176
|
+
const skillMd = join(skillsDir, skill.name, 'SKILL.md');
|
|
177
|
+
if (existsSync(skillMd)) set.add(skill.name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return [...set].sort();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
184
|
+
* diagnosePromptRouterInjection (G10)
|
|
185
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
186
|
+
|
|
187
|
+
const ROUTER_FRAMING_MARKER = '<EXTREMELY_IMPORTANT>';
|
|
188
|
+
const WRAPPER_HEADER_MARKER = '# aw-c4 router injector v1';
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Replay the Codex per-turn wrapper with synthetic stdin and assert the
|
|
192
|
+
* stdout envelope contains <EXTREMELY_IMPORTANT> framing.
|
|
193
|
+
*
|
|
194
|
+
* `simulateFirstTurn: true` (default — per spec line 996) sends a
|
|
195
|
+
* synthetic first-turn payload with no prior-turn fields, guarding against
|
|
196
|
+
* the failure mode where wrapper logic depends on prior-turn state and
|
|
197
|
+
* silently no-ops on turn 1.
|
|
198
|
+
*
|
|
199
|
+
* Note on the return shape: when `simulateFirstTurn` is false (test-only
|
|
200
|
+
* escape hatch), `firstTurnEmitsRouter` mirrors `emitsFullRouter` (no
|
|
201
|
+
* separate replay was performed). Production calls from c4Command always
|
|
202
|
+
* run with `simulateFirstTurn: true`, so the field always reflects a real
|
|
203
|
+
* first-turn probe in production.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} home
|
|
206
|
+
* @param {object} [opts]
|
|
207
|
+
* @param {boolean} [opts.simulateFirstTurn=true]
|
|
208
|
+
* @param {string} [opts.escapeEnvOverride] Forwarded as AW_PROMPT_ROUTER_INJECT (e.g. '0').
|
|
209
|
+
* @param {Function} [opts.spawnSync] Injectable for tests (defaults to node:child_process).
|
|
210
|
+
* @returns {{ wrapperRegistered: boolean, emitsFullRouter: boolean, firstTurnEmitsRouter: boolean, ok: boolean }}
|
|
211
|
+
*/
|
|
212
|
+
export function diagnosePromptRouterInjection(home, opts = {}) {
|
|
213
|
+
if (!home) throw new Error('diagnosePromptRouterInjection: home is required');
|
|
214
|
+
|
|
215
|
+
const simulateFirstTurn = opts.simulateFirstTurn !== false;
|
|
216
|
+
const spawn = opts.spawnSync ?? nodeSpawnSync;
|
|
217
|
+
const wrapperPath = join(home, '.codex/hooks/aw-user-prompt-submit.sh');
|
|
218
|
+
|
|
219
|
+
const wrapperRegistered = isWrapperRegistered(wrapperPath);
|
|
220
|
+
if (!wrapperRegistered) {
|
|
221
|
+
return { wrapperRegistered: false, emitsFullRouter: false, firstTurnEmitsRouter: false, ok: false };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const env = { ...process.env };
|
|
225
|
+
if (opts.escapeEnvOverride !== undefined) {
|
|
226
|
+
env.AW_PROMPT_ROUTER_INJECT = String(opts.escapeEnvOverride);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Steady-state replay: payload carries prior-turn fields. cwd is the
|
|
230
|
+
// injected `home` (matches firstTurnStdin) — keeps the probe hermetic and
|
|
231
|
+
// out of the host CWD.
|
|
232
|
+
const steadyOutput = invokeWrapper(spawn, wrapperPath, steadyStateStdin(home), env);
|
|
233
|
+
const emitsFullRouter = envelopeContainsRouter(steadyOutput);
|
|
234
|
+
|
|
235
|
+
// First-turn replay: minimal payload — proves the wrapper does not depend
|
|
236
|
+
// on prior-turn state to emit the router envelope. When opts.simulateFirstTurn
|
|
237
|
+
// is false, firstTurnEmitsRouter mirrors emitsFullRouter (no separate replay
|
|
238
|
+
// is performed). Production callers always run with simulateFirstTurn=true.
|
|
239
|
+
let firstTurnEmitsRouter = emitsFullRouter;
|
|
240
|
+
if (simulateFirstTurn) {
|
|
241
|
+
const firstTurnOutput = invokeWrapper(spawn, wrapperPath, firstTurnStdin(home), env);
|
|
242
|
+
firstTurnEmitsRouter = envelopeContainsRouter(firstTurnOutput);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const ok = wrapperRegistered && emitsFullRouter && firstTurnEmitsRouter;
|
|
246
|
+
return { wrapperRegistered, emitsFullRouter, firstTurnEmitsRouter, ok };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isWrapperRegistered(wrapperPath) {
|
|
250
|
+
if (!existsSync(wrapperPath)) return false;
|
|
251
|
+
try {
|
|
252
|
+
return readFileSync(wrapperPath, 'utf8').slice(0, 256).includes(WRAPPER_HEADER_MARKER);
|
|
253
|
+
} catch {
|
|
254
|
+
// Wrapper file became unreadable between existence check and read —
|
|
255
|
+
// fall back to "not registered" so the orchestrator does not try to
|
|
256
|
+
// invoke a half-broken hook.
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function invokeWrapper(spawn, wrapperPath, stdinPayload, env) {
|
|
262
|
+
const result = spawn('bash', [wrapperPath], {
|
|
263
|
+
input: stdinPayload,
|
|
264
|
+
encoding: 'utf8',
|
|
265
|
+
env,
|
|
266
|
+
});
|
|
267
|
+
return result?.stdout ?? '';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function steadyStateStdin(cwd) {
|
|
271
|
+
return JSON.stringify({
|
|
272
|
+
prompt: 'steady state diag probe',
|
|
273
|
+
cwd,
|
|
274
|
+
workspace_roots: [cwd],
|
|
275
|
+
tool_results: [],
|
|
276
|
+
previous_response_id: 'resp-prior',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function firstTurnStdin(home) {
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
prompt: 'first turn smoke',
|
|
283
|
+
cwd: home,
|
|
284
|
+
workspace_roots: [home],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function envelopeContainsRouter(output) {
|
|
289
|
+
if (typeof output !== 'string' || output.length === 0) return false;
|
|
290
|
+
// Spec invariant: full router envelope places <EXTREMELY_IMPORTANT> in the
|
|
291
|
+
// hookSpecificOutput.additionalContext field. We accept any occurrence in
|
|
292
|
+
// stdout (the wrapper builds a JSON object via python3, so framing always
|
|
293
|
+
// appears in the JSON-encoded form).
|
|
294
|
+
return output.includes(ROUTER_FRAMING_MARKER);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
298
|
+
* diagnoseSkillResolution (G2)
|
|
299
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @typedef {object} SkillResolutionResult
|
|
303
|
+
* @property {string} skill
|
|
304
|
+
* @property {string[]} candidatePathsChecked
|
|
305
|
+
* @property {string|null} found
|
|
306
|
+
* @property {boolean} ok
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* For each skill, probe four candidate paths in priority order. Top-level
|
|
311
|
+
* ok=true ONLY if the router skill (using-aw-skills) AND every stage skill
|
|
312
|
+
* (aw-plan, aw-build, aw-investigate, aw-review) resolve.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} opts
|
|
315
|
+
* @param {Harness} opts.harness
|
|
316
|
+
* @param {string} opts.home
|
|
317
|
+
* @param {string} opts.awHome
|
|
318
|
+
* @param {string} opts.eccHome
|
|
319
|
+
* @param {string[]} [opts.skills] Default: DEFAULT_SKILL_LIST
|
|
320
|
+
* @returns {{
|
|
321
|
+
* perSkill: SkillResolutionResult[],
|
|
322
|
+
* routerSkill: SkillResolutionResult,
|
|
323
|
+
* stageSkillsOk: boolean,
|
|
324
|
+
* ok: boolean
|
|
325
|
+
* }}
|
|
326
|
+
*/
|
|
327
|
+
export function diagnoseSkillResolution(opts = {}) {
|
|
328
|
+
if (!opts.harness) throw new Error('diagnoseSkillResolution: harness is required');
|
|
329
|
+
if (!opts.home) throw new Error('diagnoseSkillResolution: home is required');
|
|
330
|
+
if (!opts.awHome) throw new Error('diagnoseSkillResolution: awHome is required');
|
|
331
|
+
if (!opts.eccHome) throw new Error('diagnoseSkillResolution: eccHome is required');
|
|
332
|
+
|
|
333
|
+
const { harness, home, awHome, eccHome } = opts;
|
|
334
|
+
const skills = opts.skills ?? [...DEFAULT_SKILL_LIST];
|
|
335
|
+
|
|
336
|
+
const perSkill = skills.map((skill) => probeOneSkill(skill, harness, home, awHome, eccHome));
|
|
337
|
+
const routerSkill = perSkill.find((e) => e.skill === ROUTER_SKILL) ?? {
|
|
338
|
+
skill: ROUTER_SKILL,
|
|
339
|
+
candidatePathsChecked: [],
|
|
340
|
+
found: null,
|
|
341
|
+
ok: false,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const stageEntries = perSkill.filter((e) => e.skill !== ROUTER_SKILL);
|
|
345
|
+
const stageSkillsOk = stageEntries.length > 0 && stageEntries.every((e) => e.ok);
|
|
346
|
+
const ok = routerSkill.ok && stageSkillsOk;
|
|
347
|
+
|
|
348
|
+
return { perSkill, routerSkill, stageSkillsOk, ok };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function harnessSkillsDir(harness, home) {
|
|
352
|
+
if (harness === 'claude-web') return join(home, '.claude/skills');
|
|
353
|
+
if (harness === 'cursor-cloud') return join(home, '.cursor/skills');
|
|
354
|
+
// codex-web: registry path is the canonical source; the per-harness skills
|
|
355
|
+
// dir is conventionally absent. We still probe the registry + ECC paths.
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function probeOneSkill(skill, harness, home, awHome, eccHome) {
|
|
360
|
+
const harnessDir = harnessSkillsDir(harness, home);
|
|
361
|
+
const candidatePathsChecked = [];
|
|
362
|
+
|
|
363
|
+
if (harnessDir) {
|
|
364
|
+
candidatePathsChecked.push(join(harnessDir, skill, 'SKILL.md'));
|
|
365
|
+
candidatePathsChecked.push(join(harnessDir, `platform-core-${skill}`, 'SKILL.md'));
|
|
366
|
+
}
|
|
367
|
+
candidatePathsChecked.push(join(awHome, '.aw_registry/platform/core/skills', skill, 'SKILL.md'));
|
|
368
|
+
candidatePathsChecked.push(join(eccHome, 'skills', skill, 'SKILL.md'));
|
|
369
|
+
|
|
370
|
+
let found = null;
|
|
371
|
+
for (const p of candidatePathsChecked) {
|
|
372
|
+
if (existsSync(p)) {
|
|
373
|
+
found = p;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { skill, candidatePathsChecked, found, ok: found != null };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
382
|
+
* dumpPostInitState — stderr-only triage dump
|
|
383
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
384
|
+
|
|
385
|
+
// Covers GitHub classic PAT prefixes (ghp_, gho_, ghs_, ghu_) and the
|
|
386
|
+
// fine-grained PAT prefix github_pat_ (GA since 2023). The 8-char minimum
|
|
387
|
+
// is intentional — short enough to catch redacted-but-leaked fragments,
|
|
388
|
+
// long enough to avoid false-positive on prose like "ghx_" mentions.
|
|
389
|
+
const TOKEN_PATTERN = /(github_pat_[A-Za-z0-9_]{16,}|gh[opsu]_[A-Za-z0-9_]{8,})/g;
|
|
390
|
+
|
|
391
|
+
const DEFAULT_WRITER = {
|
|
392
|
+
stderr: (s) => process.stderr.write(s),
|
|
393
|
+
stdout: (s) => process.stdout.write(s),
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
function defaultDumpShell(cmd) {
|
|
397
|
+
const r = nodeSpawnSync(cmd, [], { shell: true, encoding: 'utf8' });
|
|
398
|
+
return {
|
|
399
|
+
ok: r.status === 0,
|
|
400
|
+
stdout: r.stdout ?? '',
|
|
401
|
+
stderr: r.stderr ?? '',
|
|
402
|
+
exitCode: r.status ?? -1,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Triage dump — token-redacted, value-redacted, stderr-only.
|
|
408
|
+
*
|
|
409
|
+
* @param {object} opts
|
|
410
|
+
* @param {Harness} opts.harness
|
|
411
|
+
* @param {string} opts.cwd
|
|
412
|
+
* @param {string} opts.awHome
|
|
413
|
+
* @param {string[]} opts.configPaths
|
|
414
|
+
* @param {object} [opts.writer] Default: { stderr: process.stderr.write, stdout: process.stdout.write }
|
|
415
|
+
* @param {Function} [opts.shell] Default: real spawnSync.
|
|
416
|
+
* @returns {void}
|
|
417
|
+
*/
|
|
418
|
+
export function dumpPostInitState(opts = {}) {
|
|
419
|
+
if (!opts.harness) throw new Error('dumpPostInitState: harness is required');
|
|
420
|
+
if (!opts.cwd) throw new Error('dumpPostInitState: cwd is required');
|
|
421
|
+
if (!opts.awHome) throw new Error('dumpPostInitState: awHome is required');
|
|
422
|
+
|
|
423
|
+
const writer = opts.writer ?? DEFAULT_WRITER;
|
|
424
|
+
const shell = typeof opts.shell === 'function' ? opts.shell : defaultDumpShell;
|
|
425
|
+
const configPaths = Array.isArray(opts.configPaths) ? opts.configPaths : [];
|
|
426
|
+
|
|
427
|
+
const lines = [];
|
|
428
|
+
lines.push('─── aw c4 post-init dump ───');
|
|
429
|
+
lines.push(`harness: ${opts.harness}`);
|
|
430
|
+
lines.push(`cwd: ${opts.cwd}`);
|
|
431
|
+
lines.push(`awHome: ${opts.awHome}`);
|
|
432
|
+
lines.push('');
|
|
433
|
+
|
|
434
|
+
// gh auth status (token-redacted).
|
|
435
|
+
lines.push('$ gh auth status --hostname github.com');
|
|
436
|
+
const ghAuth = shell('gh auth status --hostname github.com');
|
|
437
|
+
lines.push(redactTokens(ghAuth.stdout || ghAuth.stderr || '<no output>'));
|
|
438
|
+
lines.push('');
|
|
439
|
+
|
|
440
|
+
// git remote -v.
|
|
441
|
+
lines.push(`$ git -C "${opts.cwd}" remote -v`);
|
|
442
|
+
const remote = shell(`git -C "${opts.cwd}" remote -v`);
|
|
443
|
+
lines.push(redactTokens(remote.stdout || '<none>'));
|
|
444
|
+
lines.push('');
|
|
445
|
+
|
|
446
|
+
// Config-file enumeration — keys only, no values (some keys carry secrets).
|
|
447
|
+
for (const cfg of configPaths) {
|
|
448
|
+
lines.push(`$ keys-of ${cfg}`);
|
|
449
|
+
lines.push(...enumerateConfigKeysOnly(cfg));
|
|
450
|
+
lines.push('');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Registry SKILL.md sample (first 10).
|
|
454
|
+
lines.push(`$ find ${opts.awHome}/.aw_registry -name SKILL.md | head -10`);
|
|
455
|
+
for (const skillPath of sampleRegistrySkillPaths(opts.awHome, 10)) {
|
|
456
|
+
lines.push(` ${skillPath}`);
|
|
457
|
+
}
|
|
458
|
+
lines.push('─── end ───');
|
|
459
|
+
lines.push('');
|
|
460
|
+
|
|
461
|
+
writer.stderr(lines.join('\n'));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function redactTokens(text) {
|
|
465
|
+
if (typeof text !== 'string') return '';
|
|
466
|
+
return text.replace(TOKEN_PATTERN, (m) => `${m.slice(0, 4)}\u2026`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function enumerateConfigKeysOnly(filePath) {
|
|
470
|
+
if (!existsSync(filePath)) return [' <file missing>'];
|
|
471
|
+
let body;
|
|
472
|
+
try {
|
|
473
|
+
body = readFileSync(filePath, 'utf8');
|
|
474
|
+
} catch {
|
|
475
|
+
return [' <unreadable>'];
|
|
476
|
+
}
|
|
477
|
+
let parsed;
|
|
478
|
+
try {
|
|
479
|
+
parsed = JSON.parse(body);
|
|
480
|
+
} catch {
|
|
481
|
+
return [' <not JSON; key enumeration skipped>'];
|
|
482
|
+
}
|
|
483
|
+
if (parsed == null || typeof parsed !== 'object') {
|
|
484
|
+
return [' <empty or non-object>'];
|
|
485
|
+
}
|
|
486
|
+
const out = [];
|
|
487
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
488
|
+
if (v != null && typeof v === 'object' && !Array.isArray(v)) {
|
|
489
|
+
out.push(` ${k} (${Object.keys(v).length} nested keys)`);
|
|
490
|
+
} else if (Array.isArray(v)) {
|
|
491
|
+
out.push(` ${k} (array, ${v.length} entries)`);
|
|
492
|
+
} else {
|
|
493
|
+
out.push(` ${k}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function sampleRegistrySkillPaths(awHome, max) {
|
|
500
|
+
const root = join(awHome, '.aw_registry/platform');
|
|
501
|
+
if (!existsSync(root)) return [' <registry not provisioned>'];
|
|
502
|
+
const out = [];
|
|
503
|
+
walkSkillMd(root, out, max);
|
|
504
|
+
return out.length > 0 ? out : [' <no SKILL.md files>'];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function walkSkillMd(dir, out, max) {
|
|
508
|
+
if (out.length >= max) return;
|
|
509
|
+
let entries;
|
|
510
|
+
try {
|
|
511
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
512
|
+
} catch {
|
|
513
|
+
// readdir failed on a nested registry dir (EACCES, ENOENT, broken
|
|
514
|
+
// symlink target). Skip silently — the dump is best-effort triage.
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
for (const e of entries) {
|
|
518
|
+
if (out.length >= max) return;
|
|
519
|
+
const p = join(dir, e.name);
|
|
520
|
+
if (e.isFile() && e.name === 'SKILL.md') {
|
|
521
|
+
out.push(p);
|
|
522
|
+
} else if (e.isDirectory()) {
|
|
523
|
+
walkSkillMd(p, out, max);
|
|
524
|
+
} else if (e.isSymbolicLink()) {
|
|
525
|
+
// Cheap stat check — symlinks pointing at SKILL.md (the bridge case)
|
|
526
|
+
// should still appear in the dump.
|
|
527
|
+
try {
|
|
528
|
+
const st = statSync(p);
|
|
529
|
+
if (st.isFile() && e.name === 'SKILL.md') out.push(p);
|
|
530
|
+
else if (st.isDirectory()) walkSkillMd(p, out, max);
|
|
531
|
+
} catch {
|
|
532
|
+
// Broken symlink — skip.
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|