@aria_asi/cli 0.2.30 → 0.2.31

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 (87) hide show
  1. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  2. package/dist/aria-connector/src/connectors/claude-code.js +88 -20
  3. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  4. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.js +526 -2
  6. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  7. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts +7 -0
  8. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts.map +1 -0
  9. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js +87 -0
  10. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js.map +1 -0
  11. package/dist/aria-connector/src/connectors/must-read.d.ts +4 -0
  12. package/dist/aria-connector/src/connectors/must-read.d.ts.map +1 -0
  13. package/dist/aria-connector/src/connectors/must-read.js +111 -0
  14. package/dist/aria-connector/src/connectors/must-read.js.map +1 -0
  15. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  16. package/dist/aria-connector/src/connectors/opencode.js +2 -0
  17. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  18. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
  19. package/dist/aria-connector/src/connectors/runtime.js +231 -19
  20. package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
  21. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  22. package/dist/aria-connector/src/connectors/shell.js +76 -3
  23. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  24. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +52 -25
  25. package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
  26. package/dist/assets/hooks/aria-pre-tool-gate.mjs +185 -76
  27. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +63 -14
  28. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +2 -0
  29. package/dist/assets/hooks/aria-stop-gate.mjs +225 -52
  30. package/dist/assets/hooks/lib/canonical-lenses.mjs +6 -5
  31. package/dist/assets/hooks/lib/gate-loop-state.mjs +50 -0
  32. package/dist/assets/hooks/lib/hook-message-window.mjs +121 -0
  33. package/dist/assets/hooks/test-tier-lens-labeling.mjs +26 -58
  34. package/dist/assets/opencode-plugins/harness-gate/index.js +23 -1
  35. package/dist/assets/opencode-plugins/harness-stop/index.js +93 -4
  36. package/dist/runtime/auth-middleware.mjs +251 -0
  37. package/dist/runtime/codex-bridge.mjs +644 -0
  38. package/dist/runtime/discipline/CLAUDE.md +12 -0
  39. package/dist/runtime/discipline/doctrine_trigger_map.json +479 -0
  40. package/dist/runtime/doctrine_trigger_map.json +479 -0
  41. package/dist/runtime/fleet-engine.mjs +231 -0
  42. package/dist/runtime/harness-daemon.mjs +433 -0
  43. package/dist/runtime/manifest.json +1 -1
  44. package/dist/runtime/metering.mjs +100 -0
  45. package/dist/runtime/onboarding-engine.mjs +89 -0
  46. package/dist/runtime/plugin-engine.mjs +196 -0
  47. package/dist/runtime/sdk/BUNDLED.json +1 -1
  48. package/dist/runtime/sdk/index.d.ts +7 -0
  49. package/dist/runtime/sdk/index.js +120 -14
  50. package/dist/runtime/sdk/index.js.map +1 -1
  51. package/dist/runtime/service.mjs +1094 -47
  52. package/dist/runtime/workflow-engine.mjs +322 -0
  53. package/dist/sdk/BUNDLED.json +1 -1
  54. package/dist/sdk/index.d.ts +7 -0
  55. package/dist/sdk/index.js +120 -14
  56. package/dist/sdk/index.js.map +1 -1
  57. package/hooks/aria-cognition-substrate-binding.mjs +52 -25
  58. package/hooks/aria-harness-via-sdk.mjs +126 -12
  59. package/hooks/aria-pre-tool-gate.mjs +185 -76
  60. package/hooks/aria-preturn-memory-gate.mjs +63 -14
  61. package/hooks/aria-repo-doctrine-gate.mjs +2 -0
  62. package/hooks/aria-stop-gate.mjs +225 -52
  63. package/hooks/lib/canonical-lenses.mjs +6 -5
  64. package/hooks/lib/gate-loop-state.mjs +50 -0
  65. package/hooks/lib/hook-message-window.mjs +121 -0
  66. package/hooks/test-tier-lens-labeling.mjs +26 -58
  67. package/opencode-plugins/harness-gate/index.js +23 -1
  68. package/opencode-plugins/harness-stop/index.js +93 -4
  69. package/package.json +1 -1
  70. package/runtime-src/auth-middleware.mjs +251 -0
  71. package/runtime-src/codex-bridge.mjs +644 -0
  72. package/runtime-src/fleet-engine.mjs +231 -0
  73. package/runtime-src/harness-daemon.mjs +433 -0
  74. package/runtime-src/metering.mjs +100 -0
  75. package/runtime-src/onboarding-engine.mjs +89 -0
  76. package/runtime-src/plugin-engine.mjs +196 -0
  77. package/runtime-src/service.mjs +1094 -47
  78. package/runtime-src/workflow-engine.mjs +322 -0
  79. package/scripts/bundle-sdk.mjs +5 -0
  80. package/src/connectors/claude-code.ts +98 -20
  81. package/src/connectors/codex.ts +534 -1
  82. package/src/connectors/doctrine-trigger-map.ts +112 -0
  83. package/src/connectors/must-read.ts +113 -0
  84. package/src/connectors/opencode.ts +3 -0
  85. package/src/connectors/runtime.ts +241 -21
  86. package/src/connectors/shell.ts +78 -3
  87. package/dist/cli-0.2.0.tgz +0 -0
@@ -0,0 +1,322 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { randomUUID } from 'node:crypto';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const STATE_DIR = join(__dirname, '..', 'state');
8
+
9
+ function ensureDir(p) {
10
+ if (!existsSync(p)) mkdirSync(p, { recursive: true });
11
+ }
12
+
13
+ function workflowsPath(tenantId) {
14
+ return join(STATE_DIR, `workflows-${tenantId}.json`);
15
+ }
16
+
17
+ const WORKFLOW_TEMPLATES = {
18
+ 'lead-qualification-pipeline': {
19
+ id: 'lead-qualification-pipeline',
20
+ archetype: 'leadQualifier',
21
+ name: 'Lead Qualification Pipeline',
22
+ description: 'Receive inbound lead, classify, enrich data, score, and route to appropriate agent',
23
+ steps: [
24
+ { name: 'receive_and_classify', type: 'receive', description: 'Receive lead input and classify by source and intent' },
25
+ { name: 'enrich_data', type: 'enrich', description: 'Look up additional data points for the lead' },
26
+ { name: 'score_and_route', type: 'score', description: 'Score the lead and determine routing' },
27
+ { name: 'approval_gate', type: 'approve', description: 'Confirm routing decision (auto-approve if score above threshold)', autoApprove: { field: 'score', operator: '>=', value: 80 } },
28
+ { name: 'execute_routing', type: 'execute', description: 'Route lead to assigned agent or pipeline' },
29
+ { name: 'log_and_notify', type: 'log', description: 'Log the qualification result and notify relevant agents' },
30
+ ],
31
+ escalation: { to: 'directorOfSales', condition: 'score >= 90' },
32
+ retryPolicy: { maxAttempts: 3, backoffMs: 5000 },
33
+ },
34
+ 'deal-evaluation-pipeline': {
35
+ id: 'deal-evaluation-pipeline',
36
+ archetype: 'dealAnalyzer',
37
+ name: 'Deal Evaluation Pipeline',
38
+ description: 'Collect deal data, compare terms, assess risk, and provide recommendation',
39
+ steps: [
40
+ { name: 'collect_deal_data', type: 'receive', description: 'Gather all deal parameters and context' },
41
+ { name: 'compare_terms', type: 'enrich', description: 'Compare against historical deals and market benchmarks' },
42
+ { name: 'risk_assessment', type: 'score', description: 'Evaluate financial, operational, and strategic risks' },
43
+ { name: 'recommend', type: 'execute', description: 'Generate go/no-go recommendation with rationale' },
44
+ ],
45
+ escalation: { to: 'directorOfFinance', condition: 'risk_level == high' },
46
+ retryPolicy: { maxAttempts: 2, backoffMs: 10000 },
47
+ },
48
+ 'outreach-sequence': {
49
+ id: 'outreach-sequence',
50
+ archetype: 'outboundCloser',
51
+ name: 'Outreach Sequence',
52
+ description: 'Target selection, initial contact, follow-up, objection handling, and close',
53
+ steps: [
54
+ { name: 'target_selection', type: 'receive', description: 'Identify target prospects based on criteria' },
55
+ { name: 'initial_contact', type: 'execute', description: 'Send initial outreach message' },
56
+ { name: 'follow_up', type: 'execute', description: 'Send follow-up based on prospect engagement' },
57
+ { name: 'objection_handle', type: 'score', description: 'Address objections and refine approach' },
58
+ { name: 'close', type: 'execute', description: 'Attempt to close or schedule next action' },
59
+ ],
60
+ escalation: { to: 'directorOfSales', condition: 'stage == stalled > 3 days' },
61
+ retryPolicy: { maxAttempts: 5, backoffMs: 86400000 },
62
+ },
63
+ 'content-calendar': {
64
+ id: 'content-calendar',
65
+ archetype: 'contentStrategist',
66
+ name: 'Content Calendar',
67
+ description: 'Plan content topics, draft, review, publish, and measure performance',
68
+ steps: [
69
+ { name: 'plan_topics', type: 'receive', description: 'Generate content calendar based on strategy and trends' },
70
+ { name: 'draft_content', type: 'execute', description: 'Create initial content draft' },
71
+ { name: 'review', type: 'approve', description: 'Review for quality, brand alignment, and compliance' },
72
+ { name: 'publish', type: 'execute', description: 'Publish approved content to designated channels' },
73
+ { name: 'measure', type: 'score', description: 'Track performance metrics and feed back into planning' },
74
+ ],
75
+ escalation: { to: 'directorOfMarketing', condition: 'engagement < threshold' },
76
+ retryPolicy: { maxAttempts: 2, backoffMs: 0 },
77
+ },
78
+ 'deal-lifecycle': {
79
+ id: 'deal-lifecycle',
80
+ archetype: 'transactionCoordinator',
81
+ name: 'Deal Lifecycle',
82
+ description: 'Initiate deal, track milestones, coordinate stakeholders, verify, and close',
83
+ steps: [
84
+ { name: 'initiate', type: 'receive', description: 'Create deal record and assign stakeholders' },
85
+ { name: 'track_milestones', type: 'enrich', description: 'Monitor and update milestone progress' },
86
+ { name: 'coordinate', type: 'execute', description: 'Coordinate between parties and resolve blockers' },
87
+ { name: 'verify', type: 'score', description: 'Final verification of all requirements and documents' },
88
+ { name: 'close', type: 'execute', description: 'Execute close and update records' },
89
+ ],
90
+ escalation: { to: 'chiefOfStaff', condition: 'blocker unresolved > 48h' },
91
+ retryPolicy: { maxAttempts: 3, backoffMs: 3600000 },
92
+ },
93
+ 'reporting-cycle': {
94
+ id: 'reporting-cycle',
95
+ archetype: 'financialAnalyst',
96
+ name: 'Reporting Cycle',
97
+ description: 'Collect financial data, reconcile, analyze, forecast, and produce reports',
98
+ steps: [
99
+ { name: 'collect_data', type: 'receive', description: 'Gather financial data from all sources' },
100
+ { name: 'reconcile', type: 'enrich', description: 'Cross-reference and reconcile discrepancies' },
101
+ { name: 'analyze', type: 'score', description: 'Perform variance analysis and trend detection' },
102
+ { name: 'forecast', type: 'enrich', description: 'Generate forward-looking projections' },
103
+ { name: 'produce_report', type: 'execute', description: 'Compile and distribute report' },
104
+ ],
105
+ escalation: { to: 'directorOfFinance', condition: 'variance > 10%' },
106
+ retryPolicy: { maxAttempts: 2, backoffMs: 0 },
107
+ },
108
+ 'audit-trail': {
109
+ id: 'audit-trail',
110
+ archetype: 'compliance-guard',
111
+ name: 'Audit Trail',
112
+ description: 'Monitor activity, flag anomalies, investigate, remediate, and document',
113
+ steps: [
114
+ { name: 'monitor', type: 'receive', description: 'Continuously monitor agent outputs and actions' },
115
+ { name: 'flag', type: 'score', description: 'Flag potential compliance issues or anomalies' },
116
+ { name: 'investigate', type: 'enrich', description: 'Investigate flagged items for root cause' },
117
+ { name: 'remediate', type: 'execute', description: 'Apply corrective actions for confirmed violations' },
118
+ { name: 'document', type: 'log', description: 'Document findings and update compliance records' },
119
+ ],
120
+ escalation: { to: 'chiefOfStaff', condition: 'severity == critical' },
121
+ retryPolicy: { maxAttempts: 3, backoffMs: 5000 },
122
+ },
123
+ };
124
+
125
+ export function listWorkflowTemplates() {
126
+ return Object.values(WORKFLOW_TEMPLATES).map(t => ({
127
+ id: t.id,
128
+ archetype: t.archetype,
129
+ name: t.name,
130
+ description: t.description,
131
+ stepCount: t.steps.length,
132
+ hasApprovalGate: t.steps.some(s => s.type === 'approve'),
133
+ escalation: t.escalation,
134
+ }));
135
+ }
136
+
137
+ export function getWorkflowTemplate(workflowId) {
138
+ return WORKFLOW_TEMPLATES[workflowId] || null;
139
+ }
140
+
141
+ export function configureWorkflow(tenantId, workflowId, config = {}) {
142
+ const template = WORKFLOW_TEMPLATES[workflowId];
143
+ if (!template) return { ok: false, error: `Unknown workflow: ${workflowId}. Available: ${Object.keys(WORKFLOW_TEMPLATES).join(', ')}` };
144
+ const pp = workflowsPath(tenantId);
145
+ let state = existsSync(pp) ? JSON.parse(readFileSync(pp, 'utf-8')) : { configured: [], instances: [] };
146
+ const existing = state.configured.findIndex(w => w.id === workflowId);
147
+ const entry = {
148
+ id: workflowId,
149
+ templateId: workflowId,
150
+ config: {
151
+ agentId: config.agentId || null,
152
+ trigger: config.trigger || { type: 'manual' },
153
+ autoApproveThreshold: config.autoApproveThreshold ?? template.steps.find(s => s.type === 'approve')?.autoApprove?.value ?? 80,
154
+ customSteps: config.customSteps || null,
155
+ escalationOverride: config.escalationOverride || null,
156
+ ...config,
157
+ },
158
+ configuredAt: new Date().toISOString(),
159
+ };
160
+ if (existing >= 0) state.configured[existing] = entry; else state.configured.push(entry);
161
+ ensureDir(dirname(pp));
162
+ writeFileSync(pp, JSON.stringify(state, null, 2));
163
+ return { ok: true, workflow: entry };
164
+ }
165
+
166
+ export function startWorkflow(tenantId, workflowId, initialPayload = {}) {
167
+ const template = WORKFLOW_TEMPLATES[workflowId];
168
+ if (!template) return { ok: false, error: `Unknown workflow: ${workflowId}` };
169
+ const pp = workflowsPath(tenantId);
170
+ let state = existsSync(pp) ? JSON.parse(readFileSync(pp, 'utf-8')) : { configured: [], instances: [] };
171
+ const instance = {
172
+ instanceId: randomUUID(),
173
+ workflowId,
174
+ templateId: workflowId,
175
+ status: 'running',
176
+ currentStep: 0,
177
+ steps: template.steps.map((s, i) => ({
178
+ ...s,
179
+ index: i,
180
+ status: i === 0 ? 'active' : 'pending',
181
+ startedAt: i === 0 ? new Date().toISOString() : null,
182
+ completedAt: null,
183
+ attempts: 0,
184
+ result: null,
185
+ })),
186
+ payload: initialPayload,
187
+ history: [{ event: 'started', at: new Date().toISOString(), step: 0 }],
188
+ createdAt: new Date().toISOString(),
189
+ updatedAt: new Date().toISOString(),
190
+ };
191
+ state.instances.push(instance);
192
+ ensureDir(dirname(pp));
193
+ writeFileSync(pp, JSON.stringify(state, null, 2));
194
+ return { ok: true, instance };
195
+ }
196
+
197
+ export function advanceWorkflow(tenantId, instanceId, stepResult = {}) {
198
+ const pp = workflowsPath(tenantId);
199
+ if (!existsSync(pp)) return { ok: false, error: 'No workflows configured' };
200
+ let state = JSON.parse(readFileSync(pp, 'utf-8'));
201
+ const instance = (state.instances || []).find(i => i.instanceId === instanceId);
202
+ if (!instance) return { ok: false, error: `Instance ${instanceId} not found` };
203
+ if (instance.status !== 'running') return { ok: false, error: `Workflow is ${instance.status}` };
204
+ const current = instance.steps[instance.currentStep];
205
+ if (!current) return { ok: false, error: 'No current step' };
206
+ current.status = 'completed';
207
+ current.completedAt = new Date().toISOString();
208
+ current.result = stepResult;
209
+ instance.history.push({ event: 'step_completed', step: instance.currentStep, result: stepResult, at: new Date().toISOString() });
210
+ const template = WORKFLOW_TEMPLATES[instance.workflowId];
211
+ const nextIdx = instance.currentStep + 1;
212
+ if (nextIdx >= instance.steps.length) {
213
+ instance.status = 'completed';
214
+ instance.currentStep = -1;
215
+ instance.history.push({ event: 'workflow_completed', at: new Date().toISOString() });
216
+ } else {
217
+ const nextStep = instance.steps[nextIdx];
218
+ if (nextStep.type === 'approve') {
219
+ const autoApprove = nextStep.autoApprove || template?.steps[nextIdx]?.autoApprove;
220
+ const shouldAutoApprove = autoApprove && stepResult[autoApprove.field] !== undefined &&
221
+ ((autoApprove.operator === '>=' && stepResult[autoApprove.field] >= autoApprove.value) ||
222
+ (autoApprove.operator === '>' && stepResult[autoApprove.field] > autoApprove.value) ||
223
+ (autoApprove.operator === '==' && stepResult[autoApprove.field] == autoApprove.value));
224
+ if (shouldAutoApprove) {
225
+ nextStep.status = 'completed';
226
+ nextStep.completedAt = new Date().toISOString();
227
+ nextStep.result = { autoApproved: true };
228
+ instance.history.push({ event: 'auto_approved', step: nextIdx, at: new Date().toISOString() });
229
+ const afterNext = nextIdx + 1;
230
+ if (afterNext < instance.steps.length) {
231
+ instance.currentStep = afterNext;
232
+ instance.steps[afterNext].status = 'active';
233
+ instance.steps[afterNext].startedAt = new Date().toISOString();
234
+ } else {
235
+ instance.status = 'completed';
236
+ instance.currentStep = -1;
237
+ instance.history.push({ event: 'workflow_completed', at: new Date().toISOString() });
238
+ }
239
+ } else {
240
+ nextStep.status = 'awaiting_approval';
241
+ nextStep.startedAt = new Date().toISOString();
242
+ instance.currentStep = nextIdx;
243
+ instance.status = 'awaiting_approval';
244
+ instance.history.push({ event: 'awaiting_approval', step: nextIdx, at: new Date().toISOString() });
245
+ }
246
+ } else {
247
+ instance.currentStep = nextIdx;
248
+ nextStep.status = 'active';
249
+ nextStep.startedAt = new Date().toISOString();
250
+ }
251
+ }
252
+ instance.updatedAt = new Date().toISOString();
253
+ writeFileSync(pp, JSON.stringify(state, null, 2));
254
+ return { ok: true, instance };
255
+ }
256
+
257
+ export function approveWorkflowStep(tenantId, instanceId, approved = true) {
258
+ const pp = workflowsPath(tenantId);
259
+ if (!existsSync(pp)) return { ok: false, error: 'No workflows configured' };
260
+ let state = JSON.parse(readFileSync(pp, 'utf-8'));
261
+ const instance = (state.instances || []).find(i => i.instanceId === instanceId);
262
+ if (!instance) return { ok: false, error: `Instance ${instanceId} not found` };
263
+ if (instance.status !== 'awaiting_approval') return { ok: false, error: 'Workflow is not awaiting approval' };
264
+ const current = instance.steps[instance.currentStep];
265
+ if (!current) return { ok: false, error: 'No current step' };
266
+ if (approved) {
267
+ current.status = 'completed';
268
+ current.completedAt = new Date().toISOString();
269
+ current.result = { approved: true, approvedAt: new Date().toISOString() };
270
+ instance.history.push({ event: 'approved', step: instance.currentStep, at: new Date().toISOString() });
271
+ instance.status = 'running';
272
+ const nextIdx = instance.currentStep + 1;
273
+ if (nextIdx < instance.steps.length) {
274
+ instance.currentStep = nextIdx;
275
+ instance.steps[nextIdx].status = 'active';
276
+ instance.steps[nextIdx].startedAt = new Date().toISOString();
277
+ } else {
278
+ instance.status = 'completed';
279
+ instance.currentStep = -1;
280
+ instance.history.push({ event: 'workflow_completed', at: new Date().toISOString() });
281
+ }
282
+ } else {
283
+ current.status = 'rejected';
284
+ current.completedAt = new Date().toISOString();
285
+ current.result = { approved: false, rejectedAt: new Date().toISOString() };
286
+ instance.status = 'rejected';
287
+ instance.history.push({ event: 'rejected', step: instance.currentStep, at: new Date().toISOString() });
288
+ }
289
+ instance.updatedAt = new Date().toISOString();
290
+ writeFileSync(pp, JSON.stringify(state, null, 2));
291
+ return { ok: true, instance };
292
+ }
293
+
294
+ export function getWorkflowStatus(tenantId) {
295
+ const pp = workflowsPath(tenantId);
296
+ if (!existsSync(pp)) return { tenantId, configured: [], instances: [], summary: null };
297
+ const state = JSON.parse(readFileSync(pp, 'utf-8'));
298
+ const instances = state.instances || [];
299
+ const summary = {
300
+ total: instances.length,
301
+ running: instances.filter(i => i.status === 'running').length,
302
+ completed: instances.filter(i => i.status === 'completed').length,
303
+ awaitingApproval: instances.filter(i => i.status === 'awaiting_approval').length,
304
+ rejected: instances.filter(i => i.status === 'rejected').length,
305
+ };
306
+ return { tenantId, configured: state.configured || [], instances: instances.map(i => ({
307
+ instanceId: i.instanceId,
308
+ workflowId: i.workflowId,
309
+ status: i.status,
310
+ currentStep: i.currentStep >= 0 ? i.steps[i.currentStep]?.name : null,
311
+ progress: `${i.steps.filter(s => s.status === 'completed').length}/${i.steps.length}`,
312
+ createdAt: i.createdAt,
313
+ updatedAt: i.updatedAt,
314
+ })), summary };
315
+ }
316
+
317
+ export function getWorkflowInstance(tenantId, instanceId) {
318
+ const pp = workflowsPath(tenantId);
319
+ if (!existsSync(pp)) return null;
320
+ const state = JSON.parse(readFileSync(pp, 'utf-8'));
321
+ return (state.instances || []).find(i => i.instanceId === instanceId) || null;
322
+ }
@@ -32,6 +32,7 @@ const RUNTIME_DISCIPLINE_DST = resolve(RUNTIME_DST, 'discipline');
32
32
  const CONNECTOR_HOOKS_SRC = resolve(PKG_ROOT, 'hooks');
33
33
  const OPENCODE_PLUGINS_SRC = resolve(PKG_ROOT, 'opencode-plugins');
34
34
  const CLIENT_ONBOARDING_SRC = resolve(PKG_ROOT, 'CLIENT-ONBOARDING.md');
35
+ const DOCTRINE_TRIGGER_MAP_SRC = resolve(CONNECTOR_HOOKS_SRC, 'doctrine_trigger_map.json');
35
36
  const REQUIRED_HOOK_FILES = [
36
37
  'aria-harness-via-sdk.mjs',
37
38
  'aria-pre-tool-gate.mjs',
@@ -287,6 +288,10 @@ if (existsSync(SDK_SKILLS_SRC)) {
287
288
  if (existsSync(COGNITIVE_SKILLS_SRC)) {
288
289
  copyTree(COGNITIVE_SKILLS_SRC, join(RUNTIME_DISCIPLINE_DST, 'skills', 'aria-cognition'));
289
290
  }
291
+ if (existsSync(DOCTRINE_TRIGGER_MAP_SRC)) {
292
+ copyFileSync(DOCTRINE_TRIGGER_MAP_SRC, join(RUNTIME_DISCIPLINE_DST, 'doctrine_trigger_map.json'));
293
+ copyFileSync(DOCTRINE_TRIGGER_MAP_SRC, join(RUNTIME_DST, 'doctrine_trigger_map.json'));
294
+ }
290
295
  if (existsSync(CONNECTOR_HOOKS_SRC)) {
291
296
  copyTree(CONNECTOR_HOOKS_SRC, join(DIST_ASSETS_DST, 'hooks'));
292
297
  }
@@ -1,8 +1,11 @@
1
- import { existsSync, readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync, readdirSync, statSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync, readdirSync, statSync, lstatSync, realpathSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import * as path from 'path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import type { AriaConfig } from '../config.js';
6
+ import { syncDoctrineTriggerMap } from './doctrine-trigger-map.js';
7
+ import { buildMustReadGuide } from './must-read.js';
8
+ import { connectShell } from './shell.js';
6
9
 
7
10
  // ── Hooks shipped with this package ──────────────────────────────────
8
11
  // The connector installs these into ~/.claude/hooks/ so Claude Code's
@@ -241,16 +244,27 @@ const HOOKS_BLOCK = {
241
244
  }],
242
245
  };
243
246
 
244
- function installHooks(claudeDir: string, logs: string[], opts: { force?: boolean } = {}): void {
245
- const hooksTargetDir = path.join(claudeDir, 'hooks');
246
- if (!existsSync(hooksTargetDir)) {
247
- mkdirSync(hooksTargetDir, { recursive: true, mode: 0o700 });
247
+ function discoverManagedHookSymlinkTargets(hooksTargetDir: string, logs: string[]): Map<string, string> {
248
+ const targets = new Map<string, string>();
249
+ for (const name of HOOK_FILES) {
250
+ const hookPath = path.join(hooksTargetDir, name);
251
+ if (!existsSync(hookPath)) continue;
252
+ try {
253
+ const stat = lstatSync(hookPath);
254
+ if (!stat.isSymbolicLink()) continue;
255
+ const targetPath = realpathSync(hookPath);
256
+ targets.set(name, targetPath);
257
+ logs.push(`Detected managed hook symlink: ${name} -> ${targetPath}`);
258
+ } catch {}
248
259
  }
249
- const sourceDir = packageHooksDir();
260
+ return targets;
261
+ }
262
+
263
+ function installHookHelpersToRoot(sourceDir: string, targetRoot: string, logs: string[]): void {
250
264
  for (const dirName of HOOK_DIRS) {
251
265
  const srcDir = path.join(sourceDir, dirName);
252
266
  if (!existsSync(srcDir)) continue;
253
- const dstDir = path.join(hooksTargetDir, dirName);
267
+ const dstDir = path.join(targetRoot, dirName);
254
268
  if (!existsSync(dstDir)) {
255
269
  mkdirSync(dstDir, { recursive: true, mode: 0o700 });
256
270
  }
@@ -263,44 +277,76 @@ function installHooks(claudeDir: string, logs: string[], opts: { force?: boolean
263
277
  if (child.endsWith('.mjs') || child.endsWith('.js')) {
264
278
  try { chmodSync(dst, 0o755); } catch {}
265
279
  }
266
- logs.push(`Installed hook helper: ${dirName}/${child}`);
280
+ logs.push(`Installed hook helper: ${path.relative(targetRoot, dst)}`);
267
281
  }
268
282
  }
283
+ }
284
+
285
+ function installHookFilesToRoot(
286
+ sourceDir: string,
287
+ targetRoot: string,
288
+ logs: string[],
289
+ opts: { force?: boolean },
290
+ symlinkTargets: Map<string, string>,
291
+ preserveSymlinkStubs: boolean,
292
+ ): void {
269
293
  for (const name of HOOK_FILES) {
270
294
  const src = path.join(sourceDir, name);
271
295
  if (!existsSync(src)) {
272
296
  logs.push(`⚠ hook source missing: ${src} (package may be incomplete)`);
273
297
  continue;
274
298
  }
275
- const dst = path.join(hooksTargetDir, name);
276
- // Idempotent skip — if existing file matches bundled file byte-for-byte,
277
- // no overwrite + no backup churn. Re-running connect stays clean.
299
+ const dst = path.join(targetRoot, name);
300
+ if (preserveSymlinkStubs && symlinkTargets.has(name)) {
301
+ logs.push(`Preserved managed hook symlink: ${name}`);
302
+ continue;
303
+ }
278
304
  if (existsSync(dst) && !isSamePath(src, dst) && !opts.force) {
279
305
  try {
280
306
  const existingContent = readFileSync(dst, 'utf-8');
281
307
  const newContent = readFileSync(src, 'utf-8');
282
308
  if (existingContent === newContent) {
283
- logs.push(`Hook unchanged: ${name}`);
309
+ logs.push(`Hook unchanged: ${path.relative(targetRoot, dst)}`);
284
310
  continue;
285
311
  }
286
312
  } catch {/* fall through to backup+overwrite */}
287
313
  }
288
- // Back up any existing real file before overwriting (never silently
289
- // clobber a user's customizations).
290
314
  if (existsSync(dst) && !isSamePath(src, dst)) {
291
315
  const backup = `${dst}.pre-aria-connect.${Date.now()}`;
292
- try { copyFileSync(dst, backup); logs.push(`Backed up existing ${name} → ${path.basename(backup)}`); } catch {}
316
+ try { copyFileSync(dst, backup); logs.push(`Backed up existing ${path.basename(dst)} → ${path.basename(backup)}`); } catch {}
293
317
  }
294
318
  copyFileSync(src, dst);
295
- // Only the .mjs scripts need executable perms; data files (e.g. the
296
- // doctrine_trigger_map.json shipped alongside hooks) get default 0o644.
297
319
  if (name.endsWith('.mjs') || name.endsWith('.js')) {
298
320
  try { chmodSync(dst, 0o755); } catch {}
299
321
  }
300
- logs.push(`Installed hook: ${name}`);
322
+ logs.push(`Installed hook: ${path.relative(targetRoot, dst)}`);
301
323
  }
302
324
  }
303
325
 
326
+ function installHooks(claudeDir: string, logs: string[], opts: { force?: boolean } = {}): void {
327
+ const hooksTargetDir = path.join(claudeDir, 'hooks');
328
+ if (!existsSync(hooksTargetDir)) {
329
+ mkdirSync(hooksTargetDir, { recursive: true, mode: 0o700 });
330
+ }
331
+ const sourceDir = packageHooksDir();
332
+ const symlinkTargets = discoverManagedHookSymlinkTargets(hooksTargetDir, logs);
333
+ const targetRoots = new Set<string>([hooksTargetDir]);
334
+ for (const targetPath of symlinkTargets.values()) {
335
+ targetRoots.add(path.dirname(targetPath));
336
+ }
337
+ for (const root of targetRoots) {
338
+ if (!existsSync(root)) {
339
+ mkdirSync(root, { recursive: true, mode: 0o700 });
340
+ }
341
+ installHookHelpersToRoot(sourceDir, root, logs);
342
+ installHookFilesToRoot(sourceDir, root, logs, opts, symlinkTargets, root === hooksTargetDir);
343
+ }
344
+
345
+ const mustReadPath = path.join(claudeDir, 'ARIA_MUST_READ.md');
346
+ writeFileSync(mustReadPath, buildMustReadGuide('claude'), { mode: 0o644 });
347
+ logs.push(`Installed Claude must-read guide → ${mustReadPath}`);
348
+ }
349
+
304
350
  // Install bundled SDK to ~/.claude/aria-sdk/. Hooks dynamic-import from this
305
351
  // absolute path, so every fetch (validateOutput, gardenTurn, getHarnessPacket,
306
352
  // inject, checkAction) goes through the SDK control plane. Re-running connect
@@ -397,6 +443,32 @@ Record findings to your session ledger via:
397
443
  This makes findings visible to the parent session's ledger-merge hook + the auto-fix spawner.
398
444
  [/ARIA_DISCOVERY_RECORDING]`;
399
445
 
446
+ const ARIA_GATE_PHASE_DIRECTIVE = `[ARIA_GATE_SHAPE — DO THIS EVERY TURN]
447
+ PRE-GATE before the first non-trivial tool call or non-trivial answer:
448
+ 1. Read the harness packet and any [ARIA_DIRECTION] / [ARIA_BINDING_PLAN] context already injected this turn.
449
+ 2. Choose the action shape before acting: tool call, text-only answer, or replan.
450
+ 3. Emit a visible <cognition> block using the required labels for this surface. It must be readable to the user and grounded to loaded substrate anchors.
451
+ 4. If the action is destructive, deployment-related, or materially mutates state, emit a <verify> block before the tool call.
452
+ 5. If the action claims or changes an outcome, emit an <expected> block with a measurable predicate before the tool call or final answer.
453
+
454
+ MID-GATE when a hook blocks or warns:
455
+ 1. Do not repeat the same tool call or same draft unchanged.
456
+ 2. Name the exact missing requirement in one line.
457
+ 3. Re-emit the missing structure only: <cognition>, <verify>, <expected>, and/or <reflection>.
458
+ 4. Change the plan before retrying. If recovering from a repeated block, do not put the retrying tool call in the same message as the diagnosis.
459
+ 5. If the blocker is stale gate residue or stale ledger state, say that explicitly. Never invent proofOfFix or fake task resolution.
460
+
461
+ LOOP-RECOVERY after the second block of the same shape:
462
+ 1. Emit [LOOP_RECOVERY gate=<name> cause=<one line>] in plain text.
463
+ 2. Summarize what repeated, what changed, and why the next attempt is materially different.
464
+ 3. Produce the corrected preflight structure first. Retry only after the structure is complete.
465
+
466
+ OUTPUT-GATE before final emission:
467
+ 1. Keep user-visible cognition readable, but preserve the canonical lens labels. Readability comes from the prose inside each lens, never from replacing the lens names.
468
+ 2. If Mizan warns, include <reflection> addressing the exact warning instead of repeating lens text.
469
+ 3. Discoveries are atomic with their fixes. Either fix inline now or bind to a real tracked task with proof-shaped evidence.
470
+ [/ARIA_GATE_SHAPE]`;
471
+
400
472
  function buildAriaSystemBlock(config: AriaConfig): string {
401
473
  const repoList = config.repositories.map((r) => `- ${r.name} (${r.path})`).join('\n');
402
474
  const schemaText = Object.entries(config.schemaImages)
@@ -410,6 +482,7 @@ You are augmented with Aria's cognitive harness. This provides:
410
482
  - 8-lens cognition: multi-perspective analysis for every decision
411
483
 
412
484
  ${ARIA_HARNESS_BINDING_PREFIX}
485
+ ${ARIA_GATE_PHASE_DIRECTIVE}
413
486
 
414
487
  [SELF-GATE PROTOCOL]
415
488
  Before emitting any claim, verify against these hard constraints:
@@ -463,8 +536,10 @@ export async function connectClaudeCode(
463
536
  ? settings.systemPromptPrefix
464
537
  : '';
465
538
 
466
- if (existing.includes('ARIA HARNESS')) {
467
- logs.push('Aria harness prefix already present — skipping prompt injection');
539
+ const ariaBlockRx = /<!-- ARIA HARNESS.*?<!-- END ARIA HARNESS -->/s;
540
+ if (ariaBlockRx.test(existing)) {
541
+ settings.systemPromptPrefix = existing.replace(ariaBlockRx, ariaBlock);
542
+ logs.push('Refreshed Aria harness systemPromptPrefix block');
468
543
  } else {
469
544
  settings.systemPromptPrefix = existing
470
545
  ? `${ariaBlock}\n\n${existing}`
@@ -477,6 +552,7 @@ export async function connectClaudeCode(
477
552
  // just a system-prompt addendum.
478
553
  installHooks(claudeDir, logs, { force: opts.force });
479
554
  installSdk(claudeDir, logs);
555
+ syncDoctrineTriggerMap(logs);
480
556
  wireHooksBlock(settings, logs);
481
557
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 });
482
558
 
@@ -498,6 +574,8 @@ export async function connectClaudeCode(
498
574
  }
499
575
  }
500
576
 
577
+ logs.push(...await connectShell('claude', config));
578
+
501
579
  return logs;
502
580
  }
503
581