agentic-workflow-manager 9.11.12 → 9.12.1

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 (83) hide show
  1. package/dist/src/commands/doctor.js +23 -1
  2. package/dist/src/commands/hooks/claude.js +61 -8
  3. package/dist/src/commands/hooks/resync.js +1 -1
  4. package/dist/src/commands/job/gate.js +6 -1
  5. package/dist/src/commands/job/index.js +61 -5
  6. package/dist/src/commands/model-policy/index.js +253 -0
  7. package/dist/src/commands/plan/index.js +53 -9
  8. package/dist/src/commands/preflight/checks.js +18 -0
  9. package/dist/src/commands/watch/apply.js +69 -5
  10. package/dist/src/commands/watch/archive-unused.js +2 -0
  11. package/dist/src/commands/watch/index.js +24 -0
  12. package/dist/src/commands/watch/init.js +2 -1
  13. package/dist/src/commands/watch/request-recovery.js +153 -0
  14. package/dist/src/commands/watch/supervisor.js +70 -6
  15. package/dist/src/core/admission/index.js +20 -6
  16. package/dist/src/core/journal/requests.js +1 -0
  17. package/dist/src/core/journal/types.js +50 -6
  18. package/dist/src/core/model-policy/capabilities-v2.js +152 -0
  19. package/dist/src/core/model-policy/capabilities.js +7 -1
  20. package/dist/src/core/model-policy/claude-hook-events.js +120 -0
  21. package/dist/src/core/model-policy/event-store.js +221 -0
  22. package/dist/src/core/model-policy/hook-diagnostics.js +94 -0
  23. package/dist/src/core/model-policy/journal.js +113 -18
  24. package/dist/src/core/model-policy/local-claude-scope.js +41 -0
  25. package/dist/src/core/model-policy/local-codex-scope.js +33 -0
  26. package/dist/src/core/model-policy/local-event-scope.js +13 -0
  27. package/dist/src/core/model-policy/machine-key.js +56 -0
  28. package/dist/src/core/model-policy/native-claude-agent-definition.js +82 -0
  29. package/dist/src/core/model-policy/native-claude-config.js +107 -0
  30. package/dist/src/core/model-policy/native-claude-machine.js +58 -0
  31. package/dist/src/core/model-policy/native-claude-transcript.js +80 -0
  32. package/dist/src/core/model-policy/native-claude.js +41 -0
  33. package/dist/src/core/model-policy/native-codex-machine.js +116 -0
  34. package/dist/src/core/model-policy/native-codex-rollout.js +111 -0
  35. package/dist/src/core/model-policy/native-codex.js +302 -0
  36. package/dist/src/core/model-policy/native-routing-observe.js +106 -0
  37. package/dist/src/core/model-policy/native-routing-proof.js +52 -0
  38. package/dist/src/core/model-policy/native-runtime.js +86 -0
  39. package/dist/src/core/model-policy/resolve.js +96 -6
  40. package/dist/src/core/model-policy/selection-policy-digest.js +22 -0
  41. package/dist/src/core/model-policy/setup-readiness.js +77 -0
  42. package/dist/src/providers/index.js +3 -1
  43. package/dist/tests/commands/doctor-routing.test.js +22 -0
  44. package/dist/tests/commands/doctor.test.js +5 -0
  45. package/dist/tests/commands/hooks/install.test.js +6 -0
  46. package/dist/tests/commands/hooks/resync.test.js +3 -0
  47. package/dist/tests/commands/hooks/status.test.js +12 -1
  48. package/dist/tests/commands/job/routing-file.test.js +32 -0
  49. package/dist/tests/commands/job/routing-report-cli.test.js +4 -3
  50. package/dist/tests/commands/model-policy/claude-hook-flow.test.js +104 -0
  51. package/dist/tests/commands/model-policy/index.test.js +179 -0
  52. package/dist/tests/commands/preflight/preflight.test.js +65 -0
  53. package/dist/tests/commands/watch/apply.test.js +84 -0
  54. package/dist/tests/commands/watch/dispatch-routing-identity.test.js +23 -0
  55. package/dist/tests/commands/watch/request-recovery.test.js +253 -0
  56. package/dist/tests/core/admission/index.test.js +21 -0
  57. package/dist/tests/core/model-policy/capabilities-v2.test.js +42 -0
  58. package/dist/tests/core/model-policy/claude-hook-events.test.js +40 -0
  59. package/dist/tests/core/model-policy/event-store-claude.test.js +64 -0
  60. package/dist/tests/core/model-policy/event-store.test.js +151 -0
  61. package/dist/tests/core/model-policy/hook-diagnostics.test.js +34 -0
  62. package/dist/tests/core/model-policy/journal.test.js +112 -1
  63. package/dist/tests/core/model-policy/local-claude-scope.test.js +31 -0
  64. package/dist/tests/core/model-policy/machine-key.test.js +38 -0
  65. package/dist/tests/core/model-policy/native-claude-agent-definition.test.js +31 -0
  66. package/dist/tests/core/model-policy/native-claude-config.test.js +53 -0
  67. package/dist/tests/core/model-policy/native-claude-machine.test.js +18 -0
  68. package/dist/tests/core/model-policy/native-claude-transcript.test.js +45 -0
  69. package/dist/tests/core/model-policy/native-claude.test.js +21 -0
  70. package/dist/tests/core/model-policy/native-codex-machine.test.js +34 -0
  71. package/dist/tests/core/model-policy/native-codex-rollout.test.js +44 -0
  72. package/dist/tests/core/model-policy/native-codex.test.js +133 -0
  73. package/dist/tests/core/model-policy/native-routing-observe.test.js +60 -0
  74. package/dist/tests/core/model-policy/native-routing-proof.test.js +30 -0
  75. package/dist/tests/core/model-policy/native-runtime.test.js +14 -0
  76. package/dist/tests/core/model-policy/resolve-event.test.js +65 -0
  77. package/dist/tests/core/model-policy/setup-readiness.test.js +83 -0
  78. package/dist/tests/providers/index.test.js +1 -1
  79. package/package.json +2 -2
  80. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  81. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  82. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  83. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -1,24 +1,100 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveEventSelection = resolveEventSelection;
4
+ exports.resolveEventWithFallback = resolveEventWithFallback;
3
5
  exports.resolveEscalatedSelection = resolveEscalatedSelection;
4
6
  exports.resolveDispatch = resolveDispatch;
5
7
  exports.resolveV1Dispatch = resolveV1Dispatch;
6
8
  exports.resolveSelection = resolveSelection;
9
+ const crypto_1 = require("crypto");
7
10
  const capabilities_1 = require("./capabilities");
11
+ const capabilities_v2_1 = require("./capabilities-v2");
12
+ const selection_policy_digest_1 = require("./selection-policy-digest");
8
13
  const validate_1 = require("./validate");
9
14
  const validate_2 = require("../plan/validate");
15
+ /** Event-scoped v2 evidence has no clock expiry. It is never converted into a
16
+ * v1 approval or promoted to backend-model proof. */
17
+ function resolveEventSelection(input) {
18
+ if (!input || typeof input !== 'object' || !input.scope || !(input.now instanceof Date) || !Number.isFinite(input.now.getTime()))
19
+ throw new Error('event selection requires scope and finite clock');
20
+ if (!['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'].includes(input.role))
21
+ throw new Error('event selection role is invalid');
22
+ if (!['mechanical', 'integration', 'judgment', 'full'].includes(input.requestedProfile))
23
+ throw new Error('event selection profile is invalid');
24
+ if (!input.policy)
25
+ return blocked('ROUTING_POLICY_ABSENT', 'An approved routing policy is required.');
26
+ if (!input.receipt)
27
+ return blocked('ROUTING_CAPABILITY_ABSENT', 'Native event enrollment is absent.');
28
+ let policy;
29
+ try {
30
+ policy = (0, validate_1.validateApprovedPolicy)(input.policy);
31
+ }
32
+ catch {
33
+ return blocked('ROUTING_POLICY_INVALID', 'Approved routing policy is corrupt.');
34
+ }
35
+ const mapping = policy.content.mappings.find(row => row.target === input.scope.runtime.target && row.runtimeKind === input.scope.runtime.kind);
36
+ if (!mapping)
37
+ return blocked('ROUTING_POLICY_MAPPING_ABSENT', 'Approved policy has no exact target/runtime mapping.');
38
+ const effectiveProfile = input.role === 'implementer' ? input.requestedProfile : 'full';
39
+ const wanted = effectiveProfile === 'full' ? mapping.fullCapability : mapping.profiles[effectiveProfile];
40
+ let receipt;
41
+ try {
42
+ receipt = (0, capabilities_v2_1.validateEventReceipt)(input.receipt);
43
+ }
44
+ catch {
45
+ return blocked('ROUTING_CAPABILITY_INVALID', 'Native event enrollment is corrupt.');
46
+ }
47
+ let currentness;
48
+ try {
49
+ currentness = (0, capabilities_v2_1.evaluateEventReceipt)(receipt, input.scope, wanted, (0, selection_policy_digest_1.selectionPolicyDigest)(mapping, wanted), input.now);
50
+ }
51
+ catch {
52
+ return blocked('ROUTING_CAPABILITY_INVALID', 'Native event enrollment or scope is invalid.');
53
+ }
54
+ if (currentness.state === 'drift')
55
+ return blocked(`ROUTING_${currentness.reason}`, `Native event enrollment is not current: ${currentness.reason}.`);
56
+ const claim = receipt.claims.find(item => equal(item.selection, wanted));
57
+ if (!claim)
58
+ return blocked('ROUTING_SELECTION_UNENROLLED', 'Approved selection has no native event enrollment.');
59
+ if (input.scope.runtime.target === 'claude-code' && !claim.nativeAgentType)
60
+ return blocked('ROUTING_AGENT_TYPE_UNVERIFIED', 'Claude native agent type is not bound to the enrolled selection.');
61
+ if (claim.actualModel === 'unverified') {
62
+ if (!mapping.degradation.allowMissingObservedIdentity)
63
+ return blocked('ROUTING_OBSERVED_IDENTITY_UNAVAILABLE', 'Actual backend model is unverified and policy does not permit operational routing.');
64
+ return { state: 'resolved', selection: wanted, effectiveProfile, outcome: 'degraded', policyDigest: policy.contentDigest,
65
+ capabilityDigest: (0, crypto_1.createHash)('sha256').update(JSON.stringify(receipt)).digest('hex'), unavailableEvidence: ['observedModelEvidence'], ...(claim.nativeAgentType ? { nativeAgentType: claim.nativeAgentType } : {}) };
66
+ }
67
+ if (wanted.selector.kind !== 'model' || claim.actualModel.id !== wanted.selector.id)
68
+ return blocked('ROUTING_SELECTION_MISMATCH', 'Native backend model does not match the approved selection.');
69
+ return { state: 'resolved', selection: wanted, effectiveProfile, outcome: 'native', policyDigest: policy.contentDigest,
70
+ capabilityDigest: (0, crypto_1.createHash)('sha256').update(JSON.stringify(receipt)).digest('hex'), unavailableEvidence: [], ...(claim.nativeAgentType ? { nativeAgentType: claim.nativeAgentType } : {}) };
71
+ }
72
+ function resolveEventWithFallback(input) {
73
+ const preferred = resolveEventSelection(input);
74
+ if ((preferred.state === 'resolved' && !input.circuitReason) || input.role !== 'implementer' || input.requestedProfile === 'full')
75
+ return preferred;
76
+ const full = resolveEventSelection({ ...input, role: 'code-quality-reviewer', requestedProfile: 'full' });
77
+ const reason = input.circuitReason ?? (preferred.state === 'blocked' ? preferred.diagnostics[0].code : 'PROVENANCE_MISSING');
78
+ if (full.state === 'blocked')
79
+ return blocked('ROUTING_FALLBACK_UNAVAILABLE', `Optimized route ${reason} and full fallback ${full.diagnostics[0].code}; enroll the missing selection with awm model-policy setup.`);
80
+ return { ...full, effectiveProfile: 'full', outcome: 'degraded', fallbackReason: reason };
81
+ }
10
82
  function resolveEscalatedSelection(input) {
11
- if (!input || input.role !== 'implementer' || !['medium', 'high'].includes(input.expectedEffort))
83
+ if (!input || input.role !== 'implementer' || !['medium', 'high', 'runtime-default'].includes(input.expectedEffort))
12
84
  throw new Error('escalated selection requires an implementer and a supported effort');
13
- const selected = resolveSelection(input);
85
+ const selected = resolveConfiguredSelection(input);
14
86
  if (selected.state === 'blocked')
15
87
  return selected;
88
+ if (selected.fallbackReason)
89
+ return selected;
16
90
  const effort = selected.selection.effort;
91
+ if (input.expectedEffort === 'runtime-default')
92
+ return effort.kind === 'runtime-default' ? selected : blocked('ROUTING_ESCALATION_EFFORT_UNAVAILABLE', 'The approved selected effort does not match runtime-default escalation.');
17
93
  if (input.requestedProfile !== 'judgment')
18
94
  return effort.kind === 'explicit' && effort.value === input.expectedEffort ? selected : blocked('ROUTING_ESCALATION_EFFORT_UNAVAILABLE', 'The approved selected effort does not match the next lineage escalation.');
19
95
  if (effort.kind === 'explicit' && effort.value === 'medium' && input.expectedEffort === 'medium')
20
96
  return selected;
21
- const full = resolveSelection({ ...input, role: 'code-quality-reviewer', requestedProfile: 'full' });
97
+ const full = resolveConfiguredSelection({ ...input, role: 'code-quality-reviewer', requestedProfile: 'full' });
22
98
  if (full.state === 'blocked')
23
99
  return full;
24
100
  const sameModel = full.selection.selector.kind === selected.selection.selector.kind && full.selection.selector.id === selected.selection.selector.id;
@@ -30,6 +106,20 @@ function resolveEscalatedSelection(input) {
30
106
  return { ...full, effectiveProfile: 'judgment' };
31
107
  return blocked('ROUTING_ESCALATION_EFFORT_UNAVAILABLE', 'The approved selected effort does not match the next lineage escalation.');
32
108
  }
109
+ function resolveConfiguredSelection(input) {
110
+ if (input.eventReceipt !== undefined || input.eventScope !== undefined) {
111
+ if (!input.eventReceipt || !input.eventScope)
112
+ return blocked('ROUTING_EVENT_SCOPE_MISSING', 'Event-scoped routing requires both a receipt and local scope.');
113
+ const mapping = input.policy?.content.mappings.find(item => item.target === input.eventScope?.runtime.target && item.runtimeKind === input.eventScope?.runtime.kind);
114
+ const wanted = mapping && input.role === 'implementer' && input.requestedProfile !== 'full' ? mapping.profiles[input.requestedProfile] : undefined;
115
+ const incident = wanted && input.circuitIncidents?.find(item => item.target === input.eventScope?.runtime.target
116
+ && equal(item.selection, wanted)
117
+ && ['PROVIDER_REJECTED', 'SELECTION_MISMATCH', 'PROVENANCE_MISSING'].includes(item.reasonCode));
118
+ const circuitReason = incident?.reasonCode;
119
+ return resolveEventWithFallback({ role: input.role, requestedProfile: input.requestedProfile, policy: input.policy, receipt: input.eventReceipt, scope: input.eventScope, now: input.now, circuitReason });
120
+ }
121
+ return resolveSelection(input);
122
+ }
33
123
  /** Resolve only from the immutable validator report; a reopened plan may never
34
124
  * provide a routing profile after validation. */
35
125
  function resolveDispatch(input) {
@@ -49,14 +139,14 @@ function resolveDispatch(input) {
49
139
  throw new Error('local routing requires a valid slice');
50
140
  if (!local && input.sliceId !== undefined)
51
141
  throw new Error('slice is only valid for local routing');
52
- return resolveSelection({ ...input, requestedProfile: 'full' });
142
+ return resolveConfiguredSelection({ ...input, requestedProfile: 'full' });
53
143
  }
54
144
  if (typeof input.sliceId !== 'string' || input.sliceId.length === 0)
55
145
  throw new Error('implementer routing requires a slice');
56
146
  const slice = input.plan.manifest.slices.find(candidate => candidate.id === input.sliceId);
57
147
  if (!slice)
58
148
  throw new Error('routing slice does not exist in the validated plan');
59
- return resolveSelection({ ...input, requestedProfile: slice.implementerProfile });
149
+ return resolveConfiguredSelection({ ...input, requestedProfile: slice.implementerProfile });
60
150
  }
61
151
  function resolveV1Dispatch(input) {
62
152
  if (!input.plan || input.plan.schema !== 'compact-slices/v1')
@@ -64,7 +154,7 @@ function resolveV1Dispatch(input) {
64
154
  (0, validate_2.assertVerifiedValidPlanReport)(input.plan);
65
155
  if (!input.optInV1)
66
156
  return { state: 'not-required', reason: 'v1-without-opt-in' };
67
- return resolveSelection({ ...input, requestedProfile: 'full' });
157
+ return resolveConfiguredSelection({ ...input, requestedProfile: 'full' });
68
158
  }
69
159
  function equal(left, right) { return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value); }
70
160
  function blocked(code, message) { return { state: 'blocked', diagnostics: [(0, capabilities_1.routingDiagnostic)(code, message)] }; }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectionPolicyDigest = selectionPolicyDigest;
4
+ const crypto_1 = require("crypto");
5
+ const validate_1 = require("./validate");
6
+ function same(left, right) {
7
+ return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id
8
+ && left.effort.kind === right.effort.kind
9
+ && (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value);
10
+ }
11
+ /** This digest binds what native work proved: the selected model and effort.
12
+ * Degradation permission is evaluated at dispatch and may be approved later
13
+ * without forcing a second native run for the same selection. */
14
+ function selectionPolicyDigest(mapping, selection) {
15
+ const validated = (0, validate_1.validatePolicyContent)({ schema: 'model-policy/v1', mappings: [mapping],
16
+ implementationBudget: { maxAttempts: 3, escalation: ['mechanical', 'integration', 'judgment'], judgmentEfforts: ['medium', 'high'] } }).mappings[0];
17
+ const requested = [validated.profiles.mechanical, validated.profiles.integration, validated.profiles.judgment, validated.fullCapability];
18
+ const found = requested.find(item => same(item, selection));
19
+ if (!found)
20
+ throw new Error('selection is not approved in this mapping');
21
+ return (0, crypto_1.createHash)('sha256').update(JSON.stringify({ target: validated.target, runtimeKind: validated.runtimeKind, selection: found })).digest('hex');
22
+ }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routingSetupGuidance = routingSetupGuidance;
4
+ const store_1 = require("./store");
5
+ const capabilities_1 = require("./capabilities");
6
+ const json_1 = require("../plan/json");
7
+ const event_store_1 = require("./event-store");
8
+ const capabilities_v2_1 = require("./capabilities-v2");
9
+ const selection_policy_digest_1 = require("./selection-policy-digest");
10
+ /** Local guidance only: no provider process, inference, approval or filesystem write. */
11
+ function locallyCurrentReceipt(mapping, now) {
12
+ try {
13
+ const raw = (0, store_1.readBoundedCandidate)((0, capabilities_1.storedCapabilityPath)(mapping.target, mapping.runtimeKind));
14
+ if (raw === null)
15
+ return false;
16
+ const receipt = (0, capabilities_1.validateCapabilityReceipt)((0, json_1.parseJsonNoDuplicate)(raw));
17
+ const caps = receipt.capabilities;
18
+ return receipt.runtime.target === mapping.target && receipt.runtime.kind === mapping.runtimeKind
19
+ && (0, capabilities_1.receiptIsCurrent)(receipt, receipt.runtime, now)
20
+ && caps.nativeSubagents === 'supported' && caps.modelOverride === 'supported' && caps.effortOverride === 'supported'
21
+ && (caps.observedModelEvidence === 'supported' || mapping.degradation.allowMissingObservedIdentity);
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /** Passive diagnostics can check sealed coverage, but only dispatch recomputes
28
+ * runtime/account/config fingerprints. No provider process runs here. */
29
+ function locallyEnrolledEvent(mapping, now, reader) {
30
+ if (mapping.target !== 'codex' && mapping.target !== 'claude-code')
31
+ return 'absent';
32
+ try {
33
+ const stored = reader({ target: mapping.target, kind: mapping.runtimeKind, version: '0.0.0', accountScopeDigest: '0'.repeat(64) });
34
+ if (stored.state !== 'present')
35
+ return stored.state;
36
+ const receipt = stored.receipt;
37
+ if (receipt.runtime.target !== mapping.target || receipt.runtime.kind !== mapping.runtimeKind)
38
+ return 'uncovered';
39
+ const scope = { runtime: receipt.runtime, binaryDigest: receipt.binaryDigest, configDigest: receipt.configDigest };
40
+ const selections = [...Object.values(mapping.profiles), mapping.fullCapability];
41
+ return selections.every(selected => {
42
+ const current = (0, capabilities_v2_1.evaluateEventReceipt)(receipt, scope, selected, (0, selection_policy_digest_1.selectionPolicyDigest)(mapping, selected), now);
43
+ if (current.state !== 'current')
44
+ return false;
45
+ const claim = receipt.claims.find(item => JSON.stringify(item.selection) === JSON.stringify(selected));
46
+ return claim !== undefined && (claim.actualModel !== 'unverified' || mapping.degradation.allowMissingObservedIdentity);
47
+ }) ? 'covered' : 'uncovered';
48
+ }
49
+ catch {
50
+ return 'invalid';
51
+ }
52
+ }
53
+ function routingSetupGuidance(policy, now = new Date(), deps = {}) {
54
+ if (!policy || typeof policy !== 'object' || !('state' in policy))
55
+ throw new Error('routing policy state is invalid');
56
+ if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
57
+ throw new Error('routing setup clock must be finite');
58
+ if (policy.state === 'absent')
59
+ return [];
60
+ if (policy.state === 'invalid') {
61
+ if (typeof policy.reason !== 'string' || policy.reason.length === 0 || policy.reason.length > 4096)
62
+ throw new Error('routing policy invalid reason is malformed');
63
+ return [{ state: 'policy-invalid', reason: policy.reason }];
64
+ }
65
+ if (policy.state !== 'approved' || !policy.policy || !Array.isArray(policy.policy.content?.mappings))
66
+ throw new Error('approved routing policy state is malformed');
67
+ return policy.policy.content.mappings.flatMap(mapping => {
68
+ if (typeof mapping.target !== 'string' || typeof mapping.runtimeKind !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,127}$/.test(mapping.runtimeKind))
69
+ throw new Error('routing policy mapping is malformed');
70
+ const event = locallyEnrolledEvent(mapping, now, deps.readStoredEventReceipt ?? event_store_1.readStoredEventReceipt);
71
+ if (event === 'covered')
72
+ return [{ target: mapping.target, runtimeKind: mapping.runtimeKind, state: 'coverage-only' }];
73
+ if (event === 'absent' && locallyCurrentReceipt(mapping, now))
74
+ return [];
75
+ return [{ target: mapping.target, runtimeKind: mapping.runtimeKind, state: 'needs-evidence', command: `awm model-policy setup --provider ${mapping.target} --json` }];
76
+ });
77
+ }
@@ -53,10 +53,12 @@ const CONFIG_HOME = {
53
53
  // Una corrida del playbook en una maquina con CODEX_HOME seteado encontro el hook
54
54
  // instalado donde Codex no mira. Ver docs/decisions.md D-011.
55
55
  codex: { envVar: 'CODEX_HOME', dir: '.codex' },
56
+ // Claude Code documenta CLAUDE_CONFIG_DIR para mover toda su configuracion,
57
+ // incluidos settings.json y transcripciones de subagentes.
58
+ 'claude-code': { envVar: 'CLAUDE_CONFIG_DIR', dir: '.claude' },
56
59
  // Los demas usan su directorio por defecto. `null` es una afirmacion honesta —
57
60
  // "no le conocemos override"— y no un "no tiene". Cuando se confirme uno contra su
58
61
  // binario, se cambia aca y el guard obliga a que todas sus rutas lo sigan.
59
- 'claude-code': { envVar: null, dir: '.claude' },
60
62
  opencode: { envVar: null, dir: '.config/opencode' },
61
63
  cursor: { envVar: null, dir: '.cursor' },
62
64
  copilot: { envVar: null, dir: '.github' },
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const doctor_1 = require("../../src/commands/doctor");
4
+ describe('doctor routing setup', () => {
5
+ it('adds a pending, non-degrading machine configuration action only to the mapped provider', () => {
6
+ const report = {
7
+ overall: 'healthy', providers: [
8
+ { id: 'codex', label: 'Codex', tier: 'hooks-native', checks: [] },
9
+ { id: 'claude-code', label: 'Claude Code', tier: 'hooks-native', checks: [] },
10
+ ],
11
+ };
12
+ const updated = (0, doctor_1.attachRoutingSetupGuidance)(report, [
13
+ { target: 'codex', runtimeKind: 'native', state: 'needs-evidence', command: 'awm model-policy discover --provider codex --json' },
14
+ ]);
15
+ expect(updated.providers[0].checks).toEqual([
16
+ { id: 'routing.machine', state: 'pending', detail: 'native routing evidence not checked for native', remediationCode: 'awm model-policy discover --provider codex --json' },
17
+ ]);
18
+ expect(updated.providers[1].checks).toEqual([]);
19
+ expect(updated.overall).toBe('healthy');
20
+ expect(report.providers[0].checks).toEqual([]);
21
+ });
22
+ });
@@ -39,6 +39,11 @@ describe('runDoctor legacy JSON fixtures', () => {
39
39
  }
40
40
  });
41
41
  });
42
+ test('doctor labels sealed routing coverage as local-only without asking for recurring setup', () => {
43
+ const report = (0, doctor_1.attachRoutingSetupGuidance)({ overall: 'healthy', providers: [{ id: 'codex', label: 'Codex', tier: 'hooks-native', checks: [] }] }, [{ target: 'codex', runtimeKind: 'native', state: 'coverage-only' }]);
44
+ expect(report.providers[0].checks).toEqual([{ id: 'routing.machine', state: 'supported',
45
+ detail: 'sealed selection coverage for native only; current runtime/account/config scope is checked at routed dispatch' }]);
46
+ });
42
47
  describe('runDoctor dashboard modes', () => {
43
48
  it('CLI rejects --html without an argument before collection or writes', async () => {
44
49
  const program = new commander_1.Command();
@@ -59,6 +59,10 @@ describe('installHook (happy path + merge)', () => {
59
59
  expect(settings.hooks.SessionStart).toHaveLength(1);
60
60
  expect(settings.hooks.SessionStart[0].matcher).toBe('startup|clear|compact');
61
61
  expect(settings.hooks.SessionStart[0].hooks[0].command).toContain('run-hook.cmd');
62
+ expect(settings.hooks.SubagentStart[0].hooks[0].command).toBe('awm model-policy hook-event --event start');
63
+ expect(settings.hooks.SubagentStop[0].hooks[0].command).toBe('awm model-policy hook-event --event stop');
64
+ expect(settings.hooks.SubagentStart[0].hooks[0].async).toBe(false);
65
+ expect(settings.hooks.SubagentStop[0].hooks[0].async).toBe(false);
62
66
  expect(result.backupPath).toBeNull();
63
67
  });
64
68
  it('merges with pre-existing SessionStart entry from another plugin', () => {
@@ -96,6 +100,8 @@ describe('installHook (happy path + merge)', () => {
96
100
  expect(result2.status).toBe('already-up-to-date');
97
101
  const settings = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.claude/settings.json'), 'utf-8'));
98
102
  expect(settings.hooks.SessionStart).toHaveLength(1);
103
+ expect(settings.hooks.SubagentStart).toHaveLength(1);
104
+ expect(settings.hooks.SubagentStop).toHaveLength(1);
99
105
  });
100
106
  it('replaces a stale AWM entry when paths change', () => {
101
107
  const claudeDir = path_1.default.join(tmpHome, '.claude');
@@ -66,6 +66,9 @@ describe('resyncInstalledHooks', () => {
66
66
  { agent: 'claude-code', action: 'resynced' },
67
67
  { agent: 'codex', action: 'not-installed' },
68
68
  ]);
69
+ const settings = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.claude/settings.json'), 'utf8'));
70
+ expect(settings.hooks.SubagentStart[0].hooks[0].command).toBe('awm model-policy hook-event --event start');
71
+ expect(settings.hooks.SubagentStop[0].hooks[0].command).toBe('awm model-policy hook-event --event stop');
69
72
  const synced = fs_1.default.readFileSync(path_1.default.join(scriptsDir, 'session-start'), 'utf-8');
70
73
  expect(synced).toContain('NEW VERSION');
71
74
  expect(fs_1.default.lstatSync(path_1.default.join(scriptsDir, 'session-start')).isSymbolicLink()).toBe(false);
@@ -42,7 +42,9 @@ describe('computeHookStatus', () => {
42
42
  SessionStart: [{
43
43
  matcher: 'startup|clear|compact',
44
44
  hooks: [{ type: 'command', command: `${hooksDir}/run-hook.cmd session-start`, async: false }]
45
- }]
45
+ }],
46
+ SubagentStart: [{ matcher: '.+', hooks: [{ type: 'command', command: 'awm model-policy hook-event --event start', async: false }] }],
47
+ SubagentStop: [{ matcher: '.+', hooks: [{ type: 'command', command: 'awm model-policy hook-event --event stop', async: false }] }]
46
48
  }
47
49
  }, null, 2));
48
50
  }
@@ -56,6 +58,15 @@ describe('computeHookStatus', () => {
56
58
  expect(result.checks.runHookWrapper.ok).toBe(true);
57
59
  expect(result.checks.settingsEntry.ok).toBe(true);
58
60
  });
61
+ it('does not certify a non-command routing hook with matching command text', () => {
62
+ setupInstalledHook();
63
+ const settings = path_1.default.join(tmpHome, '.claude/settings.json');
64
+ const parsed = JSON.parse(fs_1.default.readFileSync(settings, 'utf8'));
65
+ parsed.hooks.SubagentStart[0].hooks[0].type = 'prompt';
66
+ fs_1.default.writeFileSync(settings, JSON.stringify(parsed));
67
+ const { computeHookStatus } = require('../../../src/commands/hooks/status');
68
+ expect(computeHookStatus('claude-code').checks.routingCapture.ok).toBe(false);
69
+ });
59
70
  it('reports DEGRADED when bootstrap skill is missing', () => {
60
71
  setupInstalledHook();
61
72
  fs_1.default.unlinkSync(path_1.default.join(tmpHome, '.awm/hooks/using-awm.md'));
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const job_1 = require("../../../src/commands/job");
10
+ describe('bounded routing input', () => {
11
+ it('rejects a file that grows between metadata check and read', () => {
12
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-routing-input-'));
13
+ const file = path_1.default.join(dir, 'observation.json');
14
+ try {
15
+ fs_1.default.writeFileSync(file, JSON.stringify({ parentThreadId: 'a'.repeat(300 * 1024) }));
16
+ const original = fs_1.default.lstatSync;
17
+ const stat = jest.spyOn(fs_1.default, 'lstatSync').mockImplementation(((target, ...args) => {
18
+ const actual = original(target, ...args);
19
+ return String(target) === file ? Object.assign(Object.create(Object.getPrototypeOf(actual)), actual, { size: 1 }) : actual;
20
+ }));
21
+ try {
22
+ expect(() => (0, job_1.readBoundedJson)(file)).toThrow(/bound|256|size|regular|large/i);
23
+ }
24
+ finally {
25
+ stat.mockRestore();
26
+ }
27
+ }
28
+ finally {
29
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
30
+ }
31
+ });
32
+ });
@@ -19,7 +19,7 @@ function gitInit(repo) {
19
19
  (0, child_process_1.execFileSync)('git', ['add', '.'], { cwd: repo });
20
20
  (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'commit', '-qm', 'fixture'], { cwd: repo });
21
21
  }
22
- test('`job routing-report --json` emits only aggregate routing evidence', async () => {
22
+ test('`job routing-report --json` exposes selections but not native child identities or envelopes', async () => {
23
23
  const repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-routing-report-'));
24
24
  try {
25
25
  gitInit(repo);
@@ -40,9 +40,10 @@ test('`job routing-report --json` emits only aggregate routing evidence', async
40
40
  await program.parseAsync(['node', 'awm', 'job', 'routing-report', '--json']);
41
41
  const rendered = String(out.mock.calls[0][0]);
42
42
  const report = JSON.parse(rendered);
43
- expect(report).toMatchObject({ schema: 'routing-report/v1', attempts: 1, plannedByRole: { implementer: 1 }, actualByRole: { implementer: 1 }, byState: { active: 1 } });
43
+ expect(report).toMatchObject({ schema: 'routing-report/v1', attempts: 1, plannedByRole: { implementer: 1 }, actualByRole: { implementer: 1 }, byState: { active: 1 },
44
+ selections: [{ configured: privateSelection, accepted: 'unknown', backendModel: 'unknown' }] });
44
45
  expect(rendered).not.toContain('native-private');
45
- expect(rendered).not.toContain('model-private');
46
+ expect(rendered).not.toContain('obligation');
46
47
  cwd.mockRestore();
47
48
  out.mockRestore();
48
49
  }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const store_1 = require("../../../src/core/model-policy/store");
11
+ const canonical_1 = require("../../../src/core/model-policy/canonical");
12
+ const machine_key_1 = require("../../../src/core/model-policy/machine-key");
13
+ const event_store_1 = require("../../../src/core/model-policy/event-store");
14
+ describe('Claude hook to event receipt integration', () => {
15
+ it('captures a real-format hook pair and assistant trace without paid inference', () => {
16
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-claude-hook-flow-'));
17
+ const previous = { home: process.env.HOME, awm: process.env.AWM_HOME, config: process.env.CLAUDE_CONFIG_DIR };
18
+ const awmHome = path_1.default.join(root, 'awm');
19
+ const project = path_1.default.join(root, 'project');
20
+ const configDir = path_1.default.join(root, 'claude');
21
+ const bin = path_1.default.join(root, 'bin');
22
+ fs_1.default.mkdirSync(awmHome);
23
+ fs_1.default.mkdirSync(project);
24
+ fs_1.default.mkdirSync(configDir);
25
+ fs_1.default.mkdirSync(bin);
26
+ process.env.HOME = root;
27
+ process.env.AWM_HOME = awmHome;
28
+ process.env.CLAUDE_CONFIG_DIR = configDir;
29
+ try {
30
+ (0, machine_key_1.ensureMachineKey)(awmHome);
31
+ const selected = { selector: { kind: 'model', id: 'claude-sonnet-4-6' }, effort: { kind: 'runtime-default' } };
32
+ const content = { schema: 'model-policy/v1', mappings: [{ target: 'claude-code', runtimeKind: 'native',
33
+ profiles: { mechanical: selected, integration: selected, judgment: selected }, fullCapability: selected,
34
+ degradation: { allowMissingModelOverride: false, allowMissingEffortOverride: false, allowMissingObservedIdentity: false } }],
35
+ implementationBudget: { maxAttempts: 3, escalation: ['mechanical', 'integration', 'judgment'], judgmentEfforts: ['medium', 'high'] } };
36
+ fs_1.default.writeFileSync(path_1.default.join(project, 'candidate.json'), JSON.stringify(content));
37
+ (0, store_1.approvePolicy)({ file: 'candidate.json', scope: 'project', cwd: project, expectedDigest: (0, canonical_1.canonicalPolicyDigest)(content) });
38
+ const executable = path_1.default.join(bin, process.platform === 'win32' ? 'claude.exe' : 'claude');
39
+ const authResponse = { loggedIn: true, authMethod: 'claude.ai', apiProvider: 'anthropic', email: 'test@example.invalid', orgId: 'org-test' };
40
+ let preload;
41
+ if (process.platform === 'win32') {
42
+ // A Unix shebang cannot be launched by CreateProcess. Use a real
43
+ // .exe and intercept only its auth-status invocation in Node.
44
+ fs_1.default.copyFileSync(process.execPath, executable);
45
+ preload = path_1.default.join(root, 'claude-preload.cjs');
46
+ fs_1.default.writeFileSync(preload, `if (require('path').basename(process.execPath).toLowerCase() === 'claude.exe' && require('path').basename(process.argv[1] || '') === 'auth') { process.stdout.write(${JSON.stringify(JSON.stringify(authResponse))}); process.exit(0); }\n`);
47
+ }
48
+ else {
49
+ fs_1.default.writeFileSync(executable, `#!${process.execPath}\nif (process.argv[2] === '--version') process.stdout.write('2.1.263\\n');\nelse if (process.argv[2] === 'auth') process.stdout.write(${JSON.stringify(JSON.stringify(authResponse))});\n`, { mode: 0o755 });
50
+ }
51
+ const timestamp = new Date().toISOString();
52
+ const transcript = path_1.default.join(configDir, 'projects', 'p', 's', 'subagents', 'agent-agent-1.jsonl');
53
+ fs_1.default.mkdirSync(path_1.default.dirname(transcript), { recursive: true });
54
+ fs_1.default.writeFileSync(transcript, `${JSON.stringify({ type: 'assistant', timestamp, message: { model: 'claude-sonnet-4-6', content: [] } })}\n`);
55
+ fs_1.default.mkdirSync(path_1.default.join(configDir, 'agents'));
56
+ fs_1.default.writeFileSync(path_1.default.join(configDir, 'agents', 'awm-integration.md'), '---\nname: awm-integration\ndescription: AWM integration agent\nmodel: claude-sonnet-4-6\n---\nPerform integration work.\n');
57
+ const start = { hook_event_name: 'SubagentStart', session_id: 'session-1', agent_id: 'agent-1', agent_type: 'awm-integration' };
58
+ const stop = { ...start, hook_event_name: 'SubagentStop', agent_transcript_path: transcript, stop_hook_active: false };
59
+ const cli = path_1.default.resolve(__dirname, '../../../dist/src/index.js');
60
+ const env = { ...process.env, PATH: `${bin}${path_1.default.delimiter}${process.env.PATH ?? ''}` };
61
+ if (preload)
62
+ env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --require=${JSON.stringify(preload)}`;
63
+ const authProbe = (0, child_process_1.spawnSync)(executable, ['auth', 'status'], { env, encoding: 'utf8' });
64
+ expect(authProbe.status).toBe(0);
65
+ expect(JSON.parse(authProbe.stdout)).toMatchObject(authResponse);
66
+ const versionProbe = (0, child_process_1.spawnSync)(executable, ['--version'], { env, encoding: 'utf8' });
67
+ expect(versionProbe.status).toBe(0);
68
+ expect(versionProbe.stdout).toMatch(/\d+\.\d+\.\d+/);
69
+ const first = (0, child_process_1.spawnSync)(process.execPath, [cli, 'model-policy', 'hook-event', '--event', 'start', '--cwd', project], { cwd: project, env, input: JSON.stringify(start), encoding: 'utf8' });
70
+ expect(first.status).toBe(0);
71
+ expect(first.stdout).toBe('');
72
+ const second = (0, child_process_1.spawnSync)(process.execPath, [cli, 'model-policy', 'hook-event', '--event', 'stop', '--cwd', project], { cwd: project, env, input: JSON.stringify(stop), encoding: 'utf8' });
73
+ expect(second.status).toBe(0);
74
+ expect(second.stdout).toBe('');
75
+ expect(second.stderr).toBe('');
76
+ const files = fs_1.default.readdirSync(path_1.default.join(awmHome, 'routing-capabilities-v2', 'claude-code'));
77
+ expect(files).toEqual(['native.json']);
78
+ const raw = fs_1.default.readFileSync(path_1.default.join(awmHome, 'routing-capabilities-v2', 'claude-code', 'native.json'), 'utf8');
79
+ expect(raw).not.toContain('test@example.invalid');
80
+ const persisted = JSON.parse(raw);
81
+ expect((0, event_store_1.readStoredEventReceipt)(persisted.receipt.runtime)).toMatchObject({ state: 'present', receipt: { claims: [expect.objectContaining({ selection: selected, nativeAgentType: 'awm-integration', actualModel: expect.objectContaining({ id: 'claude-sonnet-4-6' }), tokenUsage: 'unknown' })] } });
82
+ const generic = { ...start, agent_id: 'other-agent', agent_type: 'general-purpose' };
83
+ const ignored = (0, child_process_1.spawnSync)(process.execPath, [cli, 'model-policy', 'hook-event', '--event', 'start', '--cwd', project, '--json'], { cwd: project, env, input: JSON.stringify(generic), encoding: 'utf8' });
84
+ expect(ignored.status).toBe(0);
85
+ expect(JSON.parse(ignored.stdout)).toMatchObject({ state: 'ignored' });
86
+ expect(fs_1.default.readFileSync(path_1.default.join(awmHome, 'routing-capabilities-v2', 'claude-code', 'native.json'), 'utf8')).toBe(raw);
87
+ }
88
+ finally {
89
+ if (previous.home === undefined)
90
+ delete process.env.HOME;
91
+ else
92
+ process.env.HOME = previous.home;
93
+ if (previous.awm === undefined)
94
+ delete process.env.AWM_HOME;
95
+ else
96
+ process.env.AWM_HOME = previous.awm;
97
+ if (previous.config === undefined)
98
+ delete process.env.CLAUDE_CONFIG_DIR;
99
+ else
100
+ process.env.CLAUDE_CONFIG_DIR = previous.config;
101
+ fs_1.default.rmSync(root, { recursive: true, force: true });
102
+ }
103
+ });
104
+ });