@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.
@@ -0,0 +1,472 @@
1
+ /**
2
+ * c4/slimRouter.mjs — two slim-card variants + per-prompt ECC delegation
3
+ * + AW_SLIM_ROUTER=0 escape hatch.
4
+ *
5
+ * Why two cards: Claude Code Web invokes skills via the Skill tool (`aw:plan`,
6
+ * colon-separated), Cursor reads the SKILL.md file (`aw-plan`, hyphen-separated).
7
+ * Card text diverges only on invocation syntax, not policy. Both stay under
8
+ * SLIM_CARD_HARD_CEILING_BYTES (6144) so they fit in SessionStart inline-context.
9
+ *
10
+ * Why ECC delegation per-prompt: the slim card is SessionStart-only. The
11
+ * per-prompt reminder (UserPromptSubmit on Claude, beforeSubmitPrompt on
12
+ * Cursor) reuses ECC's existing scripts. Cursor's beforeSubmitPrompt is a
13
+ * request-rewrite hook that REQUIRES valid JSON output; using ECC's
14
+ * Node-adapter `before-submit-prompt.sh` is the only correct wiring (the
15
+ * shared text helper would emit plain text and break Cursor).
16
+ *
17
+ * Why an escape hatch: if the slim card breaks something in production,
18
+ * AW_SLIM_ROUTER=0 falls back to the full ECC SessionStart hook. No card,
19
+ * no slim-hook script — just the un-modified ECC entry-point.
20
+ *
21
+ * Contract: spec.md::§"c4/slimRouter.mjs", §"Slim card content".
22
+ */
23
+
24
+ import { mkdirSync, writeFileSync, chmodSync } from 'node:fs';
25
+ import { dirname, join } from 'node:path';
26
+ import { claudeHooksMerge, jsonMergeWithDedup } from './jsonMerge.mjs';
27
+
28
+ export const SLIM_CARD_HARD_CEILING_BYTES = 6144;
29
+
30
+ export const REQUIRED_ENFORCEMENT_PHRASES = [
31
+ 'Naming a route is not invoking it.',
32
+ 'BEFORE any other',
33
+ 'routing miss',
34
+ ];
35
+
36
+ export const REQUIRED_STAGE_MARKERS_CLAUDE = [
37
+ 'aw:review',
38
+ 'aw:plan',
39
+ 'aw:build',
40
+ 'Hard Gate',
41
+ ];
42
+
43
+ export const REQUIRED_STAGE_MARKERS_CURSOR = [
44
+ 'aw-review',
45
+ 'aw-plan',
46
+ 'aw-build',
47
+ 'Hard Gate',
48
+ ];
49
+
50
+ /* ─────────────────────────────────────────────────────────────────────────
51
+ * Card text — copied verbatim from spec.md::§"Slim card content".
52
+ * Trailing newline is intentional: Claude/Cursor parse the file as a single
53
+ * markdown document.
54
+ * ───────────────────────────────────────────────────────────────────────── */
55
+
56
+ export const SLIM_CARD_CLAUDE = `# AW Router (Compact) — using-aw-skills
57
+
58
+ You have the AW Agentic Workspace engine. This compact router replaces the
59
+ full using-aw-skills SKILL.md so it fits inside Claude Code Web's
60
+ SessionStart inline cap. The full skill is still available via the Skill
61
+ tool as \`aw:using-aw-skills\` for deep cases.
62
+
63
+ ## Hard Gate (MUST — in order, before any substantive response)
64
+
65
+ 1. Classify the user's intent (one line).
66
+ 2. Pick the SMALLEST correct stage skill from the table below.
67
+ 3. Invoke it via \`Skill(skill="aw:<stage>")\` BEFORE any other Skill, Edit,
68
+ Write, or Bash call that performs substantive work.
69
+ 4. Follow that stage skill's contract — do not improvise.
70
+
71
+ Naming a route is not invoking it. Read = call the Skill tool.
72
+
73
+ ## Intent → Stage Skill (tier 1/2 — invoke these first)
74
+
75
+ | Intent signal in the prompt | Stage skill |
76
+ |---|---|
77
+ | review, "review this PR", findings, governance, "is this ready" | \`aw:review\` |
78
+ | plan, design, refactor strategy, propose approach | \`aw:plan\` |
79
+ | build, implement, write code, add feature, TDD | \`aw:build\` |
80
+ | bug, crash, alert, regression, debug, investigate | \`aw:investigate\` |
81
+ | test, QA, e2e, coverage, regression suite | \`aw:test\` |
82
+ | deploy, release, rollout, staging | \`aw:deploy\` |
83
+ | ship, launch, rollback readiness, closeout | \`aw:ship\` |
84
+ | feature end-to-end (research → ship) | \`aw:feature\` |
85
+ | agent/skill/command/rule authoring | \`aw:adk\` |
86
+
87
+ ## Tier-3 domain skills are delegated, not invoked directly
88
+
89
+ \`platform-core-pr-review\`, \`platform-services-development\`,
90
+ \`platform-frontend-*\`, \`platform-sdet-*\`, \`platform-infra-*\`, etc. — these
91
+ are domain transports. The stage skill (\`aw:review\`, \`aw:build\`, …) decides
92
+ when to call them. Invoking a tier-3 skill before its parent stage is a
93
+ routing miss.
94
+
95
+ ## Single-reviewer fallback (review only)
96
+
97
+ You may skip \`aw:review\` and call \`platform-core-pr-review\` directly ONLY
98
+ if ALL of the following hold:
99
+
100
+ - the diff is < 50 lines AND touches a single file
101
+ - no auth, payment, schema, or public API surface
102
+ - assessed risk = Low
103
+ - the user explicitly requested single-reviewer mode
104
+
105
+ If any condition fails, route through \`aw:review\`.
106
+
107
+ ## Red flags (you skipped the gate)
108
+
109
+ - "I know the route" without calling \`Skill(skill="aw:<stage>")\`
110
+ - Pattern-matching a URL/keyword (e.g. PR link → pr-review) past the gate
111
+ - Calling a \`platform-*\` skill before its parent \`aw:*\` stage skill
112
+ - Single-reviewer review for >1 file or >50 lines without explicit request
113
+
114
+ ## Rule reminders (load on demand from disk)
115
+
116
+ Platform rules live under \`.aw/.aw_rules/platform/\` — search in this order:
117
+ \`./.aw/.aw_rules/platform/\`, \`~/.aw/.aw_rules/platform/\`, \`~/.aw_rules/platform/\`,
118
+ \`~/.aw/.aw_registry/.aw_rules/platform/\`. Read \`universal/AGENTS.md\` and
119
+ \`security/AGENTS.md\` first, then the touched-domain \`AGENTS.md\` plus
120
+ \`references/*.md\` on demand.
121
+ `;
122
+
123
+ export const SLIM_CARD_CURSOR = `# AW Router (Compact) — using-aw-skills
124
+
125
+ You have the AW Agentic Workspace engine. This compact router replaces the
126
+ full using-aw-skills SKILL.md so it fits inside the harness's SessionStart
127
+ inline-context cap. The full skill is still available on disk at
128
+ \`~/.cursor/skills/using-aw-skills/SKILL.md\` for deep cases.
129
+
130
+ ## Hard Gate (MUST — in order, before any substantive response)
131
+
132
+ 1. Classify the user's intent (one line).
133
+ 2. Pick the SMALLEST correct stage skill from the table below.
134
+ 3. Read \`~/.cursor/skills/aw-<stage>/SKILL.md\` BEFORE any other Read, Edit,
135
+ Write, Shell, or MCP call that performs substantive work.
136
+ 4. Follow that stage skill's contract — do not improvise.
137
+
138
+ Naming a route is not invoking it. Read = open the SKILL.md file.
139
+
140
+ ## Intent → Stage Skill (tier 1/2 — read these first)
141
+
142
+ | Intent signal in the prompt | Stage skill |
143
+ |---|---|
144
+ | review, "review this PR", findings, governance, "is this ready" | \`aw-review\` |
145
+ | plan, design, refactor strategy, propose approach | \`aw-plan\` |
146
+ | build, implement, write code, add feature, TDD | \`aw-build\` |
147
+ | bug, crash, alert, regression, debug, investigate | \`aw-investigate\` |
148
+ | test, QA, e2e, coverage, regression suite | \`aw-test\` |
149
+ | deploy, release, rollout, staging | \`aw-deploy\` |
150
+ | ship, launch, rollback readiness, closeout | \`aw-ship\` |
151
+ | feature end-to-end (research → ship) | \`aw-feature\` |
152
+ | agent/skill/command/rule authoring | \`aw-adk\` |
153
+
154
+ ## Tier-3 domain skills are delegated, not invoked directly
155
+
156
+ \`platform-core-pr-review\`, \`platform-services-development\`,
157
+ \`platform-frontend-*\`, \`platform-sdet-*\`, \`platform-infra-*\`, etc. — these
158
+ are domain transports. The stage skill (\`aw-review\`, \`aw-build\`, …) decides
159
+ when to call them. Reading a tier-3 skill before its parent stage is a
160
+ routing miss.
161
+
162
+ ## Single-reviewer fallback (review only)
163
+
164
+ You may skip \`aw-review\` and use \`platform-core-pr-review\` directly ONLY
165
+ if ALL of the following hold:
166
+
167
+ - the diff is < 50 lines AND touches a single file
168
+ - no auth, payment, schema, or public API surface
169
+ - assessed risk = Low
170
+ - the user explicitly requested single-reviewer mode
171
+
172
+ If any condition fails, route through \`aw-review\`.
173
+
174
+ ## Red flags (you skipped the gate)
175
+
176
+ - "I know the route" without reading \`aw-<stage>/SKILL.md\`
177
+ - Pattern-matching a URL/keyword (e.g. PR link → pr-review) past the gate
178
+ - Reading a \`platform-*\` skill before its parent \`aw-*\` stage skill
179
+ - Single-reviewer review for >1 file or >50 lines without explicit request
180
+
181
+ ## Rule reminders (load on demand from disk)
182
+
183
+ Platform rules live under \`.aw/.aw_rules/platform/\` — search in this order:
184
+ \`./.aw/.aw_rules/platform/\`, \`~/.aw/.aw_rules/platform/\`, \`~/.aw_rules/platform/\`,
185
+ \`~/.aw/.aw_registry/.aw_rules/platform/\`. Read \`universal/AGENTS.md\` and
186
+ \`security/AGENTS.md\` first, then the touched-domain \`AGENTS.md\` plus
187
+ \`references/*.md\` on demand.
188
+ `;
189
+
190
+ /* ─────────────────────────────────────────────────────────────────────────
191
+ * Hook scripts — embedded as bash one-liners that emit fixed JSON envelopes.
192
+ * ───────────────────────────────────────────────────────────────────────── */
193
+
194
+ function buildClaudeSlimHookScript(cardPath) {
195
+ // Export CARD_PATH so the python child sees it via os.environ. The python
196
+ // body is inside a SINGLE-QUOTED heredoc so no shell expansion happens —
197
+ // the path is read from the env, not interpolated, which avoids quoting
198
+ // hazards if cardPath contains shell metacharacters.
199
+ return `#!/usr/bin/env bash
200
+ # AW C4 SLIM ROUTER (Claude SessionStart) v1
201
+ set -euo pipefail
202
+ export CARD_PATH="${cardPath}"
203
+ if [ ! -f "$CARD_PATH" ]; then
204
+ printf '%s' '{}'
205
+ exit 0
206
+ fi
207
+ python3 -c '
208
+ import json, os
209
+ card = open(os.environ["CARD_PATH"]).read()
210
+ print(json.dumps({
211
+ "hookSpecificOutput": {
212
+ "hookEventName": "SessionStart",
213
+ "additionalContext": card,
214
+ }
215
+ }))
216
+ '
217
+ `;
218
+ }
219
+
220
+ function buildCursorSlimHookScript(cardPath) {
221
+ // Cursor uses the simpler top-level snake_case form. Same env-passing
222
+ // strategy as the Claude variant.
223
+ return `#!/usr/bin/env bash
224
+ # AW C4 SLIM ROUTER (Cursor sessionStart) v1
225
+ set -euo pipefail
226
+ export CARD_PATH="${cardPath}"
227
+ if [ ! -f "$CARD_PATH" ]; then
228
+ printf '%s' '{}'
229
+ exit 0
230
+ fi
231
+ python3 -c '
232
+ import json, os
233
+ card = open(os.environ["CARD_PATH"]).read()
234
+ print(json.dumps({"additional_context": card}))
235
+ '
236
+ `;
237
+ }
238
+
239
+ /* ─────────────────────────────────────────────────────────────────────────
240
+ * Public API
241
+ * ───────────────────────────────────────────────────────────────────────── */
242
+
243
+ /**
244
+ * Validate + return the slim card for a harness.
245
+ *
246
+ * @param {'claude-web'|'cursor-cloud'} harness
247
+ * @returns {{ card: string, bytes: number }}
248
+ */
249
+ export function buildSlimRouterCard(harness) {
250
+ let card;
251
+ let stageMarkers;
252
+ if (harness === 'claude-web') {
253
+ card = SLIM_CARD_CLAUDE;
254
+ stageMarkers = REQUIRED_STAGE_MARKERS_CLAUDE;
255
+ } else if (harness === 'cursor-cloud') {
256
+ card = SLIM_CARD_CURSOR;
257
+ stageMarkers = REQUIRED_STAGE_MARKERS_CURSOR;
258
+ } else {
259
+ throw new Error(`buildSlimRouterCard: unsupported harness "${harness}"`);
260
+ }
261
+
262
+ const bytes = Buffer.byteLength(card, 'utf8');
263
+ if (bytes > SLIM_CARD_HARD_CEILING_BYTES) {
264
+ throw new Error(
265
+ `Slim card for ${harness} is ${bytes} bytes, exceeds ${SLIM_CARD_HARD_CEILING_BYTES} ceiling`
266
+ );
267
+ }
268
+
269
+ for (const phrase of REQUIRED_ENFORCEMENT_PHRASES) {
270
+ if (!card.includes(phrase)) {
271
+ throw new Error(`Slim card for ${harness} missing enforcement phrase: "${phrase}"`);
272
+ }
273
+ }
274
+ for (const marker of stageMarkers) {
275
+ if (!card.includes(marker)) {
276
+ throw new Error(`Slim card for ${harness} missing stage marker: "${marker}"`);
277
+ }
278
+ }
279
+
280
+ return { card, bytes };
281
+ }
282
+
283
+ function ensureDir(d) {
284
+ mkdirSync(d, { recursive: true });
285
+ }
286
+
287
+ function writeExecutable(path, content) {
288
+ ensureDir(dirname(path));
289
+ writeFileSync(path, content, { mode: 0o755 });
290
+ try { chmodSync(path, 0o755); } catch { /* best-effort */ }
291
+ }
292
+
293
+ function isEscaped(envVarName) {
294
+ const val = process.env[envVarName];
295
+ return val === '0';
296
+ }
297
+
298
+ /**
299
+ * Install the slim router for a harness.
300
+ *
301
+ * @param {'claude-web'|'cursor-cloud'} harness
302
+ * @param {string} home
303
+ * @param {{ escapeEnvVar?: string }} [opts]
304
+ * @returns {{
305
+ * cardPath: string,
306
+ * sessionHookPath: string,
307
+ * promptHookCommand: string,
308
+ * registeredIn: string,
309
+ * escaped: boolean,
310
+ * }}
311
+ */
312
+ export function installSlimRouter(harness, home, opts = {}) {
313
+ if (harness !== 'claude-web' && harness !== 'cursor-cloud') {
314
+ throw new Error(`installSlimRouter: unsupported harness "${harness}"`);
315
+ }
316
+ if (!home || typeof home !== 'string') {
317
+ throw new Error('installSlimRouter: home is required');
318
+ }
319
+
320
+ const escapeEnvVar = opts.escapeEnvVar ?? 'AW_SLIM_ROUTER';
321
+ const escaped = isEscaped(escapeEnvVar);
322
+
323
+ if (harness === 'claude-web') {
324
+ return installClaudeSlim({ home, escaped });
325
+ }
326
+ return installCursorSlim({ home, escaped });
327
+ }
328
+
329
+ function installClaudeSlim({ home, escaped }) {
330
+ const cardPath = join(home, '.claude/aw-router-card.md');
331
+ const sessionHookPath = join(home, '.claude/hooks/aw-slim-session-start.sh');
332
+ const settingsPath = join(home, '.claude/settings.json');
333
+
334
+ // Ensure the registry-config parent dir exists in BOTH modes — escape mode
335
+ // skips the card write that would otherwise create it, but settings.json
336
+ // still needs to land somewhere.
337
+ ensureDir(dirname(settingsPath));
338
+
339
+ // Per-prompt: ECC's existing rule-reminder script (NOT a card re-emitter).
340
+ const promptHookCommand =
341
+ `bash -lc 'exec bash "\${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/hooks/session-start-rules-context.sh"'`;
342
+
343
+ if (!escaped) {
344
+ ensureDir(dirname(cardPath));
345
+ writeFileSync(cardPath, SLIM_CARD_CLAUDE, { mode: 0o600 });
346
+
347
+ const hookScript = buildClaudeSlimHookScript(cardPath);
348
+ writeExecutable(sessionHookPath, hookScript);
349
+
350
+ const sessionCommand = `bash -lc 'exec bash "${sessionHookPath}"'`;
351
+
352
+ claudeHooksMerge({
353
+ settingsPath,
354
+ eventName: 'SessionStart',
355
+ matcher: 'startup|clear|compact',
356
+ command: sessionCommand,
357
+ description: 'AW slim router (SessionStart)',
358
+ commandPatterns: [
359
+ 'aw-slim-session-start.sh',
360
+ 'hooks/session-start',
361
+ ],
362
+ });
363
+ } else {
364
+ // Escape: register the full ECC SessionStart entry-point.
365
+ const fullEcc =
366
+ `bash -lc 'exec bash "\${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/hooks/session-start"'`;
367
+ claudeHooksMerge({
368
+ settingsPath,
369
+ eventName: 'SessionStart',
370
+ matcher: 'startup|clear|compact',
371
+ command: fullEcc,
372
+ description: 'AW full ECC SessionStart (slim escape hatch)',
373
+ commandPatterns: [
374
+ 'aw-slim-session-start.sh',
375
+ 'hooks/session-start',
376
+ ],
377
+ });
378
+ }
379
+
380
+ // Per-prompt hook is registered in BOTH default and escape modes — the
381
+ // reminder is independent of the slim card.
382
+ claudeHooksMerge({
383
+ settingsPath,
384
+ eventName: 'UserPromptSubmit',
385
+ command: promptHookCommand,
386
+ description: 'AW per-prompt rules reminder (ECC delegation)',
387
+ commandPatterns: ['session-start-rules-context.sh'],
388
+ });
389
+
390
+ return {
391
+ cardPath,
392
+ sessionHookPath,
393
+ promptHookCommand,
394
+ registeredIn: settingsPath,
395
+ escaped,
396
+ };
397
+ }
398
+
399
+ function installCursorSlim({ home, escaped }) {
400
+ const cardPath = join(home, '.cursor/aw-router-card.md');
401
+ const sessionHookPath = join(home, '.cursor/hooks/aw-slim-session-start.sh');
402
+ const hooksJsonPath = join(home, '.cursor/hooks.json');
403
+
404
+ // Same rationale as installClaudeSlim — escape mode skips the card write.
405
+ ensureDir(dirname(hooksJsonPath));
406
+
407
+ // (G1) The Node-adapter wrapper that satisfies Cursor's request-rewrite
408
+ // JSON contract. NEVER delegate to shared/user-prompt-submit.sh directly.
409
+ const promptHookCommand =
410
+ `bash -lc 'f="$PWD/.cursor/hooks/before-submit-prompt.sh"; ` +
411
+ `[ -f "$f" ] || f="$HOME/.cursor/hooks/before-submit-prompt.sh"; exec bash "$f"'`;
412
+
413
+ if (!escaped) {
414
+ ensureDir(dirname(cardPath));
415
+ writeFileSync(cardPath, SLIM_CARD_CURSOR, { mode: 0o600 });
416
+
417
+ const hookScript = buildCursorSlimHookScript(cardPath);
418
+ writeExecutable(sessionHookPath, hookScript);
419
+
420
+ const sessionCommand = `bash -lc 'exec bash "${sessionHookPath}"'`;
421
+
422
+ jsonMergeWithDedup({
423
+ filePath: hooksJsonPath,
424
+ jsonPointer: '/hooks/sessionStart',
425
+ newEntry: {
426
+ command: sessionCommand,
427
+ event: 'sessionStart',
428
+ description: 'AW slim router (sessionStart)',
429
+ },
430
+ commandPatterns: [
431
+ 'aw-slim-session-start.sh',
432
+ '.cursor/hooks/session-start.sh',
433
+ ],
434
+ });
435
+ } else {
436
+ const fullEcc =
437
+ `bash -lc 'f="$PWD/.cursor/hooks/session-start.sh"; ` +
438
+ `[ -f "$f" ] || f="$HOME/.cursor/hooks/session-start.sh"; exec bash "$f"'`;
439
+ jsonMergeWithDedup({
440
+ filePath: hooksJsonPath,
441
+ jsonPointer: '/hooks/sessionStart',
442
+ newEntry: {
443
+ command: fullEcc,
444
+ event: 'sessionStart',
445
+ description: 'AW full ECC sessionStart (slim escape hatch)',
446
+ },
447
+ commandPatterns: [
448
+ 'aw-slim-session-start.sh',
449
+ '.cursor/hooks/session-start.sh',
450
+ ],
451
+ });
452
+ }
453
+
454
+ jsonMergeWithDedup({
455
+ filePath: hooksJsonPath,
456
+ jsonPointer: '/hooks/beforeSubmitPrompt',
457
+ newEntry: {
458
+ command: promptHookCommand,
459
+ event: 'beforeSubmitPrompt',
460
+ description: 'AW per-prompt rules reminder (ECC Node-adapter)',
461
+ },
462
+ commandPatterns: ['before-submit-prompt.sh'],
463
+ });
464
+
465
+ return {
466
+ cardPath,
467
+ sessionHookPath,
468
+ promptHookCommand,
469
+ registeredIn: hooksJsonPath,
470
+ escaped,
471
+ };
472
+ }
package/cli.mjs CHANGED
@@ -27,6 +27,7 @@ const COMMANDS = {
27
27
  daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
28
28
  telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
29
29
  'slack-sim': () => import('./commands/slack-sim.mjs').then(m => m.slackSimCommand),
30
+ c4: () => import('./commands/c4.mjs').then(m => m.c4Command),
30
31
  };
31
32
 
32
33
  function parseArgs(argv) {
@@ -115,6 +116,12 @@ function printHelp() {
115
116
  cmd('aw slack-sim run <scenario>', 'Replay Slack-like scenarios against real runtime'),
116
117
  cmd('aw slack-sim list-scenarios', 'List built-in Slack simulator scenarios'),
117
118
 
119
+ sec('Cloud'),
120
+ cmd('aw c4', 'Bootstrap AW for cloud harnesses (Claude/Cursor/Codex)'),
121
+ cmd('aw c4 --dry-run', 'Run preflight + auth setup without installing'),
122
+ cmd('aw c4 --diagnose', 'Self-test post-init wiring (always exits 0)'),
123
+ cmd('aw c4 --harness <name>', 'Override harness detection (cursor-cloud|codex-web|claude-web)'),
124
+
118
125
  sec('Settings'),
119
126
  cmd('aw telemetry status', 'Show telemetry status'),
120
127
  cmd('aw telemetry disable', 'Opt out of anonymous analytics'),