@thejoseki/clawform 2.3.0 → 2.3.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.
Files changed (48) hide show
  1. package/README.md +141 -120
  2. package/bin/clawform.js +161 -151
  3. package/package.json +65 -65
  4. package/src/aws/catalog.js +116 -116
  5. package/src/aws/changeset.js +231 -231
  6. package/src/aws/drift.js +59 -59
  7. package/src/aws/exports.js +213 -213
  8. package/src/aws/inventory.js +156 -156
  9. package/src/commands/activate.js +34 -2
  10. package/src/commands/ci-init.js +393 -212
  11. package/src/commands/cost-report.js +163 -163
  12. package/src/commands/deactivate.js +9 -2
  13. package/src/commands/deploy.js +413 -413
  14. package/src/commands/diff.js +39 -39
  15. package/src/commands/drift.js +35 -35
  16. package/src/commands/init-plugin.js +6 -6
  17. package/src/commands/init.js +360 -360
  18. package/src/commands/rollback.js +126 -126
  19. package/src/commands/status.js +147 -147
  20. package/src/commands/sync.js +50 -50
  21. package/src/commands/update.js +24 -0
  22. package/src/commands/validate.js +349 -349
  23. package/src/hooks/block-dangerous.js +62 -62
  24. package/src/hooks/cfn-lint-after-edit-eval.js +30 -30
  25. package/src/hooks/cfn-lint-after-edit.js +81 -81
  26. package/src/hooks/check-catalog-fresh-eval.js +63 -63
  27. package/src/hooks/check-catalog-fresh.js +33 -33
  28. package/src/hooks/session-context-eval.js +81 -81
  29. package/src/hooks/session-context.js +41 -41
  30. package/src/hooks/subagent-context-eval.js +44 -44
  31. package/src/hooks/subagent-context.js +48 -48
  32. package/src/lib/audit-synthesis.js +24 -24
  33. package/src/lib/aws-client.js +15 -15
  34. package/src/lib/cfn-yaml.js +92 -92
  35. package/src/lib/config.js +76 -76
  36. package/src/lib/constants.js +85 -85
  37. package/src/lib/defaults.js +16 -16
  38. package/src/lib/install-workflow.js +28 -28
  39. package/src/lib/kit-source.js +43 -5
  40. package/src/lib/license-client.js +84 -84
  41. package/src/lib/license-config.js +162 -162
  42. package/src/lib/license-store.js +43 -8
  43. package/src/lib/license.js +15 -2
  44. package/src/lib/lint-overrides.js +226 -226
  45. package/src/lib/logger.js +16 -16
  46. package/src/lib/project-config.js +145 -145
  47. package/src/lib/providers/polar.js +215 -215
  48. package/src/lib/session-state.js +91 -91
@@ -1,81 +1,81 @@
1
- // Pure decision logic for the SessionStart context hook. Emits a single terse
2
- // block (customer, envs, region, legacy-prefix count, catalog freshness) when
3
- // the current project resolves an clawform.config.json; silent otherwise, so
4
- // a non-clawform project never sees noise.
5
- //
6
- // Never includes account IDs or AWS profile — CLAUDE.md's identity rule says
7
- // "Never echo the resolved values back in chat", and those live in
8
- // clawform.config.json `accounts` / .claude/settings.local.json, not here.
9
- //
10
- // Stays pure: takes (deps) and returns { shouldEmit, text }. The shim
11
- // supplies real findConfigPath/loadProjectConfig/statSync; tests inject fakes.
12
-
13
- import { STALE_HOURS, resolveCatalogPath } from './check-catalog-fresh-eval.js';
14
- import { STALE_CHANGESET_DAYS } from '../lib/session-state.js';
15
-
16
- // readState defaults to a no-op empty state so existing callers/tests that
17
- // don't care about in-flight state keep working; the shim injects the real one.
18
- export function evaluate({
19
- cwd,
20
- now,
21
- findConfigPath,
22
- loadProjectConfig,
23
- statSync,
24
- readState = () => ({ pendingChangesets: [] }),
25
- }) {
26
- const configPath = findConfigPath(cwd);
27
- if (!configPath) return { shouldEmit: false, text: '' };
28
-
29
- const cfg = loadProjectConfig({ projectDir: cwd });
30
- const customer = cfg.customer || '(unset)';
31
- const envs = Array.isArray(cfg.envs) && cfg.envs.length ? cfg.envs.join(',') : '(none)';
32
- const region = cfg.region || '(unset)';
33
- const legacyCount = Array.isArray(cfg.legacy_prefixes) ? cfg.legacy_prefixes.length : 0;
34
-
35
- const parts = [
36
- `customer=${customer}`,
37
- `envs=${envs}`,
38
- `region=${region}`,
39
- `legacy-prefixes=${legacyCount}`,
40
- catalogFreshnessPart(cwd, now, statSync),
41
- ];
42
-
43
- const pending = pendingChangesetPart(cwd, now, readState);
44
- if (pending) parts.push(pending);
45
-
46
- return { shouldEmit: true, text: `[clawform] ${parts.join(' ')}` };
47
- }
48
-
49
- // Warn about any changeset created but not executed (survives /clear) — the
50
- // next operator sees it and can execute or clean it up. Fail-open: a bad
51
- // state file yields no line, never an exception.
52
- function pendingChangesetPart(cwd, now, readState) {
53
- let state;
54
- try {
55
- state = readState(cwd);
56
- } catch {
57
- return '';
58
- }
59
- const pending = Array.isArray(state?.pendingChangesets) ? state.pendingChangesets : [];
60
- if (pending.length === 0) return '';
61
- const descs = pending.map((e) => {
62
- const ageDays = (now - new Date(e.createdAt).getTime()) / 86_400_000;
63
- // A corrupt/hand-edited createdAt yields NaN — show it as stale rather than
64
- // "(NaNd)" so the operator still gets a "clean this up" signal.
65
- if (!Number.isFinite(ageDays)) return `${e.stack}/${e.changeSetName}(?!stale)`;
66
- const stale = ageDays > STALE_CHANGESET_DAYS ? '!stale' : '';
67
- return `${e.stack}/${e.changeSetName}(${Math.max(0, Math.round(ageDays))}d${stale})`;
68
- });
69
- return `pending-changesets=${descs.join(',')}`;
70
- }
71
-
72
- function catalogFreshnessPart(cwd, now, statSync) {
73
- let stat;
74
- try {
75
- stat = statSync(resolveCatalogPath(cwd));
76
- } catch {
77
- return 'catalog=missing';
78
- }
79
- const ageHours = (now - new Date(stat.mtime).getTime()) / 3_600_000;
80
- return ageHours > STALE_HOURS ? `catalog=stale(${Math.round(ageHours)}h)` : 'catalog=fresh';
81
- }
1
+ // Pure decision logic for the SessionStart context hook. Emits a single terse
2
+ // block (customer, envs, region, legacy-prefix count, catalog freshness) when
3
+ // the current project resolves an clawform.config.json; silent otherwise, so
4
+ // a non-clawform project never sees noise.
5
+ //
6
+ // Never includes account IDs or AWS profile — CLAUDE.md's identity rule says
7
+ // "Never echo the resolved values back in chat", and those live in
8
+ // clawform.config.json `accounts` / .claude/settings.local.json, not here.
9
+ //
10
+ // Stays pure: takes (deps) and returns { shouldEmit, text }. The shim
11
+ // supplies real findConfigPath/loadProjectConfig/statSync; tests inject fakes.
12
+
13
+ import { STALE_HOURS, resolveCatalogPath } from './check-catalog-fresh-eval.js';
14
+ import { STALE_CHANGESET_DAYS } from '../lib/session-state.js';
15
+
16
+ // readState defaults to a no-op empty state so existing callers/tests that
17
+ // don't care about in-flight state keep working; the shim injects the real one.
18
+ export function evaluate({
19
+ cwd,
20
+ now,
21
+ findConfigPath,
22
+ loadProjectConfig,
23
+ statSync,
24
+ readState = () => ({ pendingChangesets: [] }),
25
+ }) {
26
+ const configPath = findConfigPath(cwd);
27
+ if (!configPath) return { shouldEmit: false, text: '' };
28
+
29
+ const cfg = loadProjectConfig({ projectDir: cwd });
30
+ const customer = cfg.customer || '(unset)';
31
+ const envs = Array.isArray(cfg.envs) && cfg.envs.length ? cfg.envs.join(',') : '(none)';
32
+ const region = cfg.region || '(unset)';
33
+ const legacyCount = Array.isArray(cfg.legacy_prefixes) ? cfg.legacy_prefixes.length : 0;
34
+
35
+ const parts = [
36
+ `customer=${customer}`,
37
+ `envs=${envs}`,
38
+ `region=${region}`,
39
+ `legacy-prefixes=${legacyCount}`,
40
+ catalogFreshnessPart(cwd, now, statSync),
41
+ ];
42
+
43
+ const pending = pendingChangesetPart(cwd, now, readState);
44
+ if (pending) parts.push(pending);
45
+
46
+ return { shouldEmit: true, text: `[clawform] ${parts.join(' ')}` };
47
+ }
48
+
49
+ // Warn about any changeset created but not executed (survives /clear) — the
50
+ // next operator sees it and can execute or clean it up. Fail-open: a bad
51
+ // state file yields no line, never an exception.
52
+ function pendingChangesetPart(cwd, now, readState) {
53
+ let state;
54
+ try {
55
+ state = readState(cwd);
56
+ } catch {
57
+ return '';
58
+ }
59
+ const pending = Array.isArray(state?.pendingChangesets) ? state.pendingChangesets : [];
60
+ if (pending.length === 0) return '';
61
+ const descs = pending.map((e) => {
62
+ const ageDays = (now - new Date(e.createdAt).getTime()) / 86_400_000;
63
+ // A corrupt/hand-edited createdAt yields NaN — show it as stale rather than
64
+ // "(NaNd)" so the operator still gets a "clean this up" signal.
65
+ if (!Number.isFinite(ageDays)) return `${e.stack}/${e.changeSetName}(?!stale)`;
66
+ const stale = ageDays > STALE_CHANGESET_DAYS ? '!stale' : '';
67
+ return `${e.stack}/${e.changeSetName}(${Math.max(0, Math.round(ageDays))}d${stale})`;
68
+ });
69
+ return `pending-changesets=${descs.join(',')}`;
70
+ }
71
+
72
+ function catalogFreshnessPart(cwd, now, statSync) {
73
+ let stat;
74
+ try {
75
+ stat = statSync(resolveCatalogPath(cwd));
76
+ } catch {
77
+ return 'catalog=missing';
78
+ }
79
+ const ageHours = (now - new Date(stat.mtime).getTime()) / 3_600_000;
80
+ return ageHours > STALE_HOURS ? `catalog=stale(${Math.round(ageHours)}h)` : 'catalog=fresh';
81
+ }
@@ -1,41 +1,41 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Clawform SessionStart hook — prints a terse config+catalog context block
4
- * when the current project resolves an clawform.config.json; silent (exit 0,
5
- * no output) for any other project, so a bare dir sees nothing.
6
- *
7
- * Resolves the project dir the same way block-dangerous.js does: prefer the
8
- * hook event's own `cwd`, then CLAUDE_PROJECT_DIR, then process.cwd().
9
- *
10
- * Fail-LOUD on event-parse error, and on consumer-config load error (bad
11
- * JSON / wrong-shaped keys — project-config.js throws loudly on both), both
12
- * stderr + exit 0 — never blocks the session; SessionStart output is
13
- * advisory only.
14
- */
15
-
16
- import { readFileSync, statSync } from 'node:fs';
17
- import { evaluate } from './session-context-eval.js';
18
- import { findConfigPath, loadProjectConfig } from '../lib/project-config.js';
19
- import { readState } from '../lib/session-state.js';
20
-
21
- let event;
22
- try {
23
- event = JSON.parse(readFileSync(0, 'utf8'));
24
- } catch (err) {
25
- process.stderr.write(`[clawform session-context] event parse failed (silent): ${err.message}\n`);
26
- process.exit(0);
27
- }
28
-
29
- const cwd = (typeof event?.cwd === 'string' && event.cwd) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
30
-
31
- let shouldEmit = false;
32
- let text = '';
33
- try {
34
- ({ shouldEmit, text } = evaluate({ cwd, now: Date.now(), findConfigPath, loadProjectConfig, statSync, readState }));
35
- } catch (err) {
36
- process.stderr.write(`[clawform session-context] consumer config load failed at ${cwd} (silent): ${err.message}\n`);
37
- }
38
-
39
- if (shouldEmit) {
40
- console.log(text);
41
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Clawform SessionStart hook — prints a terse config+catalog context block
4
+ * when the current project resolves an clawform.config.json; silent (exit 0,
5
+ * no output) for any other project, so a bare dir sees nothing.
6
+ *
7
+ * Resolves the project dir the same way block-dangerous.js does: prefer the
8
+ * hook event's own `cwd`, then CLAUDE_PROJECT_DIR, then process.cwd().
9
+ *
10
+ * Fail-LOUD on event-parse error, and on consumer-config load error (bad
11
+ * JSON / wrong-shaped keys — project-config.js throws loudly on both), both
12
+ * stderr + exit 0 — never blocks the session; SessionStart output is
13
+ * advisory only.
14
+ */
15
+
16
+ import { readFileSync, statSync } from 'node:fs';
17
+ import { evaluate } from './session-context-eval.js';
18
+ import { findConfigPath, loadProjectConfig } from '../lib/project-config.js';
19
+ import { readState } from '../lib/session-state.js';
20
+
21
+ let event;
22
+ try {
23
+ event = JSON.parse(readFileSync(0, 'utf8'));
24
+ } catch (err) {
25
+ process.stderr.write(`[clawform session-context] event parse failed (silent): ${err.message}\n`);
26
+ process.exit(0);
27
+ }
28
+
29
+ const cwd = (typeof event?.cwd === 'string' && event.cwd) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
30
+
31
+ let shouldEmit = false;
32
+ let text = '';
33
+ try {
34
+ ({ shouldEmit, text } = evaluate({ cwd, now: Date.now(), findConfigPath, loadProjectConfig, statSync, readState }));
35
+ } catch (err) {
36
+ process.stderr.write(`[clawform session-context] consumer config load failed at ${cwd} (silent): ${err.message}\n`);
37
+ }
38
+
39
+ if (shouldEmit) {
40
+ console.log(text);
41
+ }
@@ -1,44 +1,44 @@
1
- // Pure decision logic for the SubagentStart rule-injection hook. When the
2
- // current project resolves an clawform.config.json, every spawned subagent
3
- // receives Clawform's 6 hard rules + naming convention as injected context —
4
- // so a subagent generating or reviewing CloudFormation inherits the same
5
- // invariants the main session operates under, WITHOUT each workflow having to
6
- // embed them in every prompt by hand.
7
- //
8
- // Silent when no config resolves (a non-clawform project's subagents see
9
- // nothing). Never emits account IDs, AWS profile, or raw legacy-prefix values
10
- // (per CLAUDE.md identity rule) — only the customer shortname, which is
11
- // already public in every stack name.
12
- //
13
- // Stays pure: takes injected findConfigPath/loadProjectConfig; the shim
14
- // supplies the real implementations, tests supply fakes.
15
-
16
- // The 6 hard rules from CLAUDE.md, compressed to one line each. This is the
17
- // canonical terse copy; workflows/audit-all-stacks.js keeps its own embedded
18
- // copy as a defense-in-depth fallback (the hook can be disabled) — keep the
19
- // two in rough sync, but this one is what subagents actually receive.
20
- export const HARD_RULES_TERSE = `Clawform hard rules — all subagent work must comply:
21
- 1. Never modify legacy stacks (frozen prefixes in clawform.config.json legacy_prefixes) without a "# FORCE LEGACY: <reason>" line.
22
- 2. Never hardcode credentials in CFN Parameters — use {{resolve:secretsmanager:<name>:SecretString:<key>}}.
23
- 3. Never "!ImportValue !Sub" (invalid YAML) — use long form Fn::ImportValue: !Sub "\${EnvName}-...".
24
- 4. Stateful resources (RDS/S3/DynamoDB/EFS/ElastiCache/Logs::LogGroup/SecretsManager::Secret/KMS::Key/Cognito) need DeletionPolicy + UpdateReplacePolicy set to a non-destroy value.
25
- 5. No direct deploy — CreateChangeSet -> describe -> confirm -> ExecuteChangeSet.
26
- 6. No "aws cloudformation delete-stack" outside the /rollback command.
27
- Full rules: CLAUDE.md.`;
28
-
29
- function namingLine(customer, envs) {
30
- const c = customer || '<customer>';
31
- // Emit the consumer's actual env list (matching session-context's fidelity)
32
- // so a subagent doesn't propose an env the consumer narrowed away.
33
- const envList = Array.isArray(envs) && envs.length ? envs.join('/') : 'd/stg/p';
34
- return `Naming: <env>-${c}-<pri|pub>-<service>-<project>-<feature>-[zone] (env ${envList}; service shortnames in rules/cfn-standards.md).`;
35
- }
36
-
37
- export function evaluate({ cwd, findConfigPath, loadProjectConfig }) {
38
- const configPath = findConfigPath(cwd);
39
- if (!configPath) return { shouldEmit: false, text: '' };
40
-
41
- const cfg = loadProjectConfig({ projectDir: cwd });
42
- const text = `${HARD_RULES_TERSE}\n${namingLine(cfg.customer, cfg.envs)}`;
43
- return { shouldEmit: true, text };
44
- }
1
+ // Pure decision logic for the SubagentStart rule-injection hook. When the
2
+ // current project resolves an clawform.config.json, every spawned subagent
3
+ // receives Clawform's 6 hard rules + naming convention as injected context —
4
+ // so a subagent generating or reviewing CloudFormation inherits the same
5
+ // invariants the main session operates under, WITHOUT each workflow having to
6
+ // embed them in every prompt by hand.
7
+ //
8
+ // Silent when no config resolves (a non-clawform project's subagents see
9
+ // nothing). Never emits account IDs, AWS profile, or raw legacy-prefix values
10
+ // (per CLAUDE.md identity rule) — only the customer shortname, which is
11
+ // already public in every stack name.
12
+ //
13
+ // Stays pure: takes injected findConfigPath/loadProjectConfig; the shim
14
+ // supplies the real implementations, tests supply fakes.
15
+
16
+ // The 6 hard rules from CLAUDE.md, compressed to one line each. This is the
17
+ // canonical terse copy; workflows/audit-all-stacks.js keeps its own embedded
18
+ // copy as a defense-in-depth fallback (the hook can be disabled) — keep the
19
+ // two in rough sync, but this one is what subagents actually receive.
20
+ export const HARD_RULES_TERSE = `Clawform hard rules — all subagent work must comply:
21
+ 1. Never modify legacy stacks (frozen prefixes in clawform.config.json legacy_prefixes) without a "# FORCE LEGACY: <reason>" line.
22
+ 2. Never hardcode credentials in CFN Parameters — use {{resolve:secretsmanager:<name>:SecretString:<key>}}.
23
+ 3. Never "!ImportValue !Sub" (invalid YAML) — use long form Fn::ImportValue: !Sub "\${EnvName}-...".
24
+ 4. Stateful resources (RDS/S3/DynamoDB/EFS/ElastiCache/Logs::LogGroup/SecretsManager::Secret/KMS::Key/Cognito) need DeletionPolicy + UpdateReplacePolicy set to a non-destroy value.
25
+ 5. No direct deploy — CreateChangeSet -> describe -> confirm -> ExecuteChangeSet.
26
+ 6. No "aws cloudformation delete-stack" outside the /rollback command.
27
+ Full rules: CLAUDE.md.`;
28
+
29
+ function namingLine(customer, envs) {
30
+ const c = customer || '<customer>';
31
+ // Emit the consumer's actual env list (matching session-context's fidelity)
32
+ // so a subagent doesn't propose an env the consumer narrowed away.
33
+ const envList = Array.isArray(envs) && envs.length ? envs.join('/') : 'd/stg/p';
34
+ return `Naming: <env>-${c}-<pri|pub>-<service>-<project>-<feature>-[zone] (env ${envList}; service shortnames in rules/cfn-standards.md).`;
35
+ }
36
+
37
+ export function evaluate({ cwd, findConfigPath, loadProjectConfig }) {
38
+ const configPath = findConfigPath(cwd);
39
+ if (!configPath) return { shouldEmit: false, text: '' };
40
+
41
+ const cfg = loadProjectConfig({ projectDir: cwd });
42
+ const text = `${HARD_RULES_TERSE}\n${namingLine(cfg.customer, cfg.envs)}`;
43
+ return { shouldEmit: true, text };
44
+ }
@@ -1,48 +1,48 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Clawform SubagentStart hook — injects the 6 hard rules + naming convention
4
- * into every subagent when the current project resolves an clawform.config.json;
5
- * silent (exit 0, no output) for any other project.
6
- *
7
- * SubagentStart requires the structured envelope
8
- * { hookSpecificOutput: { hookEventName: "SubagentStart", additionalContext } }
9
- * on stdout — plain stdout (as SessionStart accepts) is NOT injected here.
10
- *
11
- * Resolves the project dir like the other hooks: prefer the event's own `cwd`,
12
- * then CLAUDE_PROJECT_DIR, then process.cwd().
13
- *
14
- * Fail-LOUD on event-parse error, and on consumer-config load error (bad JSON /
15
- * wrong-shaped keys — project-config.js throws on both), both stderr + exit 0 —
16
- * never blocks the subagent; injected context is advisory only.
17
- */
18
-
19
- import { readFileSync } from 'node:fs';
20
- import { evaluate } from './subagent-context-eval.js';
21
- import { findConfigPath, loadProjectConfig } from '../lib/project-config.js';
22
-
23
- let event;
24
- try {
25
- event = JSON.parse(readFileSync(0, 'utf8'));
26
- } catch (err) {
27
- process.stderr.write(`[clawform subagent-context] event parse failed (silent): ${err.message}\n`);
28
- process.exit(0);
29
- }
30
-
31
- const cwd = (typeof event?.cwd === 'string' && event.cwd) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
32
-
33
- let shouldEmit = false;
34
- let text = '';
35
- try {
36
- ({ shouldEmit, text } = evaluate({ cwd, findConfigPath, loadProjectConfig }));
37
- } catch (err) {
38
- process.stderr.write(`[clawform subagent-context] consumer config load failed at ${cwd} (silent): ${err.message}\n`);
39
- }
40
-
41
- if (shouldEmit) {
42
- console.log(JSON.stringify({
43
- hookSpecificOutput: {
44
- hookEventName: 'SubagentStart',
45
- additionalContext: text,
46
- },
47
- }));
48
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Clawform SubagentStart hook — injects the 6 hard rules + naming convention
4
+ * into every subagent when the current project resolves an clawform.config.json;
5
+ * silent (exit 0, no output) for any other project.
6
+ *
7
+ * SubagentStart requires the structured envelope
8
+ * { hookSpecificOutput: { hookEventName: "SubagentStart", additionalContext } }
9
+ * on stdout — plain stdout (as SessionStart accepts) is NOT injected here.
10
+ *
11
+ * Resolves the project dir like the other hooks: prefer the event's own `cwd`,
12
+ * then CLAUDE_PROJECT_DIR, then process.cwd().
13
+ *
14
+ * Fail-LOUD on event-parse error, and on consumer-config load error (bad JSON /
15
+ * wrong-shaped keys — project-config.js throws on both), both stderr + exit 0 —
16
+ * never blocks the subagent; injected context is advisory only.
17
+ */
18
+
19
+ import { readFileSync } from 'node:fs';
20
+ import { evaluate } from './subagent-context-eval.js';
21
+ import { findConfigPath, loadProjectConfig } from '../lib/project-config.js';
22
+
23
+ let event;
24
+ try {
25
+ event = JSON.parse(readFileSync(0, 'utf8'));
26
+ } catch (err) {
27
+ process.stderr.write(`[clawform subagent-context] event parse failed (silent): ${err.message}\n`);
28
+ process.exit(0);
29
+ }
30
+
31
+ const cwd = (typeof event?.cwd === 'string' && event.cwd) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
32
+
33
+ let shouldEmit = false;
34
+ let text = '';
35
+ try {
36
+ ({ shouldEmit, text } = evaluate({ cwd, findConfigPath, loadProjectConfig }));
37
+ } catch (err) {
38
+ process.stderr.write(`[clawform subagent-context] consumer config load failed at ${cwd} (silent): ${err.message}\n`);
39
+ }
40
+
41
+ if (shouldEmit) {
42
+ console.log(JSON.stringify({
43
+ hookSpecificOutput: {
44
+ hookEventName: 'SubagentStart',
45
+ additionalContext: text,
46
+ },
47
+ }));
48
+ }
@@ -1,24 +1,24 @@
1
- // Pure merge/rank logic for the project-audit workflow (workflows/audit-all-stacks.js).
2
- // Kept separate from the workflow body because agent-call orchestration isn't
3
- // unit-testable — this is. Each stack's review-stack / iam-audit / cost-guard
4
- // verdict rolls up to that resource's worst finding, matching the existing
5
- // iam-audit convention ("Score the whole resource at its worst finding").
6
-
7
- const SEVERITY_RANK = { '🔴': 3, '🟡': 2, '✅': 1 };
8
-
9
- export function rollupStackSeverity(perStack) {
10
- const severities = [perStack.review?.severity, perStack.iam?.severity, perStack.cost?.severity].filter(Boolean);
11
- if (severities.length === 0) return '✅';
12
- return severities.reduce((worst, s) => (SEVERITY_RANK[s] > SEVERITY_RANK[worst] ? s : worst), '✅');
13
- }
14
-
15
- export function synthesizeAudit(perStackResults) {
16
- const stacks = perStackResults
17
- .map((r) => ({ ...r, severity: rollupStackSeverity(r) }))
18
- .sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || a.stack.localeCompare(b.stack));
19
-
20
- const summary = { total: stacks.length, '🔴': 0, '🟡': 0, '✅': 0 };
21
- for (const s of stacks) summary[s.severity] += 1;
22
-
23
- return { summary, stacks };
24
- }
1
+ // Pure merge/rank logic for the project-audit workflow (workflows/audit-all-stacks.js).
2
+ // Kept separate from the workflow body because agent-call orchestration isn't
3
+ // unit-testable — this is. Each stack's review-stack / iam-audit / cost-guard
4
+ // verdict rolls up to that resource's worst finding, matching the existing
5
+ // iam-audit convention ("Score the whole resource at its worst finding").
6
+
7
+ const SEVERITY_RANK = { '🔴': 3, '🟡': 2, '✅': 1 };
8
+
9
+ export function rollupStackSeverity(perStack) {
10
+ const severities = [perStack.review?.severity, perStack.iam?.severity, perStack.cost?.severity].filter(Boolean);
11
+ if (severities.length === 0) return '✅';
12
+ return severities.reduce((worst, s) => (SEVERITY_RANK[s] > SEVERITY_RANK[worst] ? s : worst), '✅');
13
+ }
14
+
15
+ export function synthesizeAudit(perStackResults) {
16
+ const stacks = perStackResults
17
+ .map((r) => ({ ...r, severity: rollupStackSeverity(r) }))
18
+ .sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || a.stack.localeCompare(b.stack));
19
+
20
+ const summary = { total: stacks.length, '🔴': 0, '🟡': 0, '✅': 0 };
21
+ for (const s of stacks) summary[s.severity] += 1;
22
+
23
+ return { summary, stacks };
24
+ }
@@ -1,15 +1,15 @@
1
- import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
2
- import { loadConfig } from './config.js';
3
-
4
- /**
5
- * Return a CloudFormation client bound to the user's profile & region.
6
- * Profile is propagated via `AWS_PROFILE` env var so both the SDK's default
7
- * credential chain AND any `execa`-spawned `aws` CLI child inherit it.
8
- */
9
- export function cfnClient(cliOverrides = {}) {
10
- const cfg = loadConfig(cliOverrides);
11
- // Mutate process.env so any execa-spawned `aws` CLI child inherits the same profile.
12
- // Last-write-wins across callers; acceptable for a short-lived CLI process.
13
- if (cfg.AWS_PROFILE) process.env.AWS_PROFILE = cfg.AWS_PROFILE;
14
- return new CloudFormationClient({ region: cfg.AWS_REGION });
15
- }
1
+ import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
2
+ import { loadConfig } from './config.js';
3
+
4
+ /**
5
+ * Return a CloudFormation client bound to the user's profile & region.
6
+ * Profile is propagated via `AWS_PROFILE` env var so both the SDK's default
7
+ * credential chain AND any `execa`-spawned `aws` CLI child inherit it.
8
+ */
9
+ export function cfnClient(cliOverrides = {}) {
10
+ const cfg = loadConfig(cliOverrides);
11
+ // Mutate process.env so any execa-spawned `aws` CLI child inherits the same profile.
12
+ // Last-write-wins across callers; acceptable for a short-lived CLI process.
13
+ if (cfg.AWS_PROFILE) process.env.AWS_PROFILE = cfg.AWS_PROFILE;
14
+ return new CloudFormationClient({ region: cfg.AWS_REGION });
15
+ }