intentdna 1.5.2 → 1.5.3

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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "DNA template compilation + runtime enforcement",
12
- "version": "1.5.2",
12
+ "version": "1.5.3",
13
13
  "source": "./"
14
14
  }
15
15
  ]
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Declarative policy layer for AI agent governance"
5
5
  }
@@ -483,6 +483,8 @@ export async function runSync(opts) {
483
483
  if (removed > 0) {
484
484
  process.stderr.write(`Cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
485
485
  }
486
+ // G2/G3: Register built-in DNA MCP server in .claude/.mcp.json
487
+ await registerDNAMCPServer(cwd);
486
488
  }
487
489
  // Step 3.6: Register hooks in settings.json (bin mode only)
488
490
  // Plugin mode: hooks managed by plugin framework — skip settings.json
@@ -680,3 +682,28 @@ async function writeMCPConfig(mcpDeps, mcpJsonPath, variables) {
680
682
  function substituteVariables(template, vars) {
681
683
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
682
684
  }
685
+ /**
686
+ * G2/G3: Register the built-in DNA MCP server (dna-mcp) in .claude/.mcp.json.
687
+ * Idempotent: only adds if not already present.
688
+ */
689
+ async function registerDNAMCPServer(cwd) {
690
+ const mcpJsonPath = resolve(cwd, ".claude", ".mcp.json");
691
+ let existing = {};
692
+ try {
693
+ const raw = await readFile(mcpJsonPath, "utf-8");
694
+ existing = JSON.parse(raw);
695
+ }
696
+ catch { /* doesn't exist yet */ }
697
+ const mcpServers = (existing.mcpServers ?? {});
698
+ // Don't overwrite if user already configured it
699
+ if (mcpServers["intentdna"])
700
+ return;
701
+ mcpServers["intentdna"] = {
702
+ command: "dna-mcp",
703
+ args: ["--project-dir", cwd],
704
+ };
705
+ existing.mcpServers = mcpServers;
706
+ await mkdir(dirname(mcpJsonPath), { recursive: true });
707
+ await writeFileAsync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
708
+ process.stderr.write(`MCP: registered dna-mcp server in ${mcpJsonPath}\n`);
709
+ }
@@ -219,6 +219,7 @@ export function compileDNA(activated, cascaded) {
219
219
  const wfStepCheckpoints = [];
220
220
  const activeRoles = [];
221
221
  const handoffChain = [];
222
+ const stepEnforceRules = [];
222
223
  for (const step of wf.steps ?? []) {
223
224
  if (!activeRoles.includes(step.role)) {
224
225
  activeRoles.push(step.role);
@@ -238,6 +239,15 @@ export function compileDNA(activated, cascaded) {
238
239
  consumes: step.handoff.consumes,
239
240
  });
240
241
  }
242
+ // G4: Collect step enforce rules
243
+ if (step.enforce) {
244
+ stepEnforceRules.push({
245
+ step_id: step.id,
246
+ read_only: step.enforce.read_only,
247
+ relax_after_iteration: step.enforce.relax_after_iteration,
248
+ additional_write_paths: step.enforce.additional_write_paths,
249
+ });
250
+ }
241
251
  }
242
252
  // Derive namespace from workflow key: "ns_wfname" → "ns", "wfname" → ""
243
253
  const underscoreIdx = wfKey.indexOf("_");
@@ -248,6 +258,7 @@ export function compileDNA(activated, cascaded) {
248
258
  step_checkpoints: wfStepCheckpoints,
249
259
  active_roles: activeRoles,
250
260
  handoff_chain: handoffChain,
261
+ step_enforce_rules: stepEnforceRules.length > 0 ? stepEnforceRules : undefined,
251
262
  });
252
263
  }
253
264
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Intent DNA — Remote Governance Module
3
+ *
4
+ * Architecture reservation for Phase 7 enterprise governance.
5
+ * Currently exports type definitions only — no runtime implementation.
6
+ */
7
+ export type { AuditReport, RemoteDNAPolicy, PolicyContent, PolicyScope, PolicyUpdate, GovernanceConfig, GovernanceClient, GovernanceResponse, GovernanceSubscription, } from "./types.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Intent DNA — Remote Governance Module
3
+ *
4
+ * Architecture reservation for Phase 7 enterprise governance.
5
+ * Currently exports type definitions only — no runtime implementation.
6
+ */
7
+ export {};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Intent DNA — Remote Governance Types
3
+ *
4
+ * Architecture reservation for enterprise governance communication.
5
+ * Phase 7 implementation — current phase only defines interfaces.
6
+ *
7
+ * Capabilities:
8
+ * - Audit data reporting to central server
9
+ * - Enterprise DNA policy pull
10
+ * - Policy update push notifications
11
+ */
12
+ /** Audit report sent from local DNA to governance server */
13
+ export interface AuditReport {
14
+ /** Report ID (UUID) */
15
+ report_id: string;
16
+ /** ISO timestamp */
17
+ timestamp: string;
18
+ /** Session that generated the report */
19
+ session_id: string;
20
+ /** Project identifier */
21
+ project_id: string;
22
+ /** Summary statistics */
23
+ stats: {
24
+ total_events: number;
25
+ blocks: number;
26
+ warns: number;
27
+ allows: number;
28
+ };
29
+ /** Top blocked tools */
30
+ top_blocked_tools: Array<{
31
+ tool: string;
32
+ count: number;
33
+ }>;
34
+ /** Top blocked paths */
35
+ top_blocked_paths: Array<{
36
+ path: string;
37
+ count: number;
38
+ }>;
39
+ /** Active DNA template IDs */
40
+ active_templates: string[];
41
+ /** IR version in use */
42
+ ir_version: number;
43
+ }
44
+ /** Enterprise DNA policy pulled from governance server */
45
+ export interface RemoteDNAPolicy {
46
+ /** Policy ID */
47
+ policy_id: string;
48
+ /** Policy version (semver) */
49
+ version: string;
50
+ /** When the policy was last updated */
51
+ updated_at: string;
52
+ /** Priority level (higher overrides lower) */
53
+ priority: number;
54
+ /** The DNA content (can be inlined or a URL reference) */
55
+ dna: PolicyContent;
56
+ /** Which projects/teams this policy applies to */
57
+ scope: PolicyScope;
58
+ /** Whether this policy is mandatory (cannot be overridden locally) */
59
+ mandatory: boolean;
60
+ }
61
+ /** Policy content — either inline DNA JSON or a reference */
62
+ export interface PolicyContent {
63
+ /** Inline DNA JSON */
64
+ inline?: Record<string, unknown>;
65
+ /** URL to fetch DNA from */
66
+ url?: string;
67
+ /** SHA-256 hash for integrity verification */
68
+ sha256?: string;
69
+ }
70
+ /** Scope of a policy — which projects/teams it applies to */
71
+ export interface PolicyScope {
72
+ /** Apply to all projects */
73
+ all?: boolean;
74
+ /** Apply to specific project IDs */
75
+ projects?: string[];
76
+ /** Apply to specific team names */
77
+ teams?: string[];
78
+ /** Apply to projects matching glob patterns */
79
+ project_patterns?: string[];
80
+ }
81
+ /** Policy update push notification */
82
+ export interface PolicyUpdate {
83
+ /** Update type */
84
+ type: "created" | "updated" | "revoked";
85
+ /** The policy that changed */
86
+ policy: RemoteDNAPolicy;
87
+ /** Change description */
88
+ change_description?: string;
89
+ /** Whether immediate re-sync is required */
90
+ requires_resync: boolean;
91
+ }
92
+ /** Configuration for connecting to a governance server */
93
+ export interface GovernanceConfig {
94
+ /** Server URL */
95
+ server_url: string;
96
+ /** Authentication token or API key */
97
+ auth_token?: string;
98
+ /** Organization ID */
99
+ org_id: string;
100
+ /** Polling interval in seconds (for pull mode) */
101
+ poll_interval_s?: number;
102
+ /** Enable push notifications via WebSocket */
103
+ push_enabled?: boolean;
104
+ /** TLS certificate path (for mTLS) */
105
+ tls_cert?: string;
106
+ }
107
+ /** Governance client interface — to be implemented in Phase 7 */
108
+ export interface GovernanceClient {
109
+ /** Report audit data to the governance server */
110
+ reportAudit(report: AuditReport): Promise<GovernanceResponse>;
111
+ /** Pull latest policies from the governance server */
112
+ pullPolicies(): Promise<RemoteDNAPolicy[]>;
113
+ /** Subscribe to policy updates (push mode) */
114
+ subscribePolicyUpdates(callback: (update: PolicyUpdate) => void): Promise<GovernanceSubscription>;
115
+ /** Check connectivity to the governance server */
116
+ healthCheck(): Promise<boolean>;
117
+ }
118
+ /** Response from governance server operations */
119
+ export interface GovernanceResponse {
120
+ /** Whether the operation succeeded */
121
+ ok: boolean;
122
+ /** Error message if failed */
123
+ error?: string;
124
+ /** Server-side request ID for debugging */
125
+ request_id?: string;
126
+ }
127
+ /** Subscription handle for push updates */
128
+ export interface GovernanceSubscription {
129
+ /** Unique subscription ID */
130
+ id: string;
131
+ /** Unsubscribe from updates */
132
+ unsubscribe(): Promise<void>;
133
+ /** Whether the subscription is active */
134
+ active: boolean;
135
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Intent DNA — Remote Governance Types
3
+ *
4
+ * Architecture reservation for enterprise governance communication.
5
+ * Phase 7 implementation — current phase only defines interfaces.
6
+ *
7
+ * Capabilities:
8
+ * - Audit data reporting to central server
9
+ * - Enterprise DNA policy pull
10
+ * - Policy update push notifications
11
+ */
12
+ export {};
package/dist/hooks/cli.js CHANGED
@@ -101,6 +101,7 @@ async function main() {
101
101
  workflow: wfState.workflow,
102
102
  current_role: wfState.current_role,
103
103
  completed_artifacts: wfState.completed_artifacts,
104
+ iteration: wfState.iteration, // G4: pass iteration for state-driven rules
104
105
  };
105
106
  // Re-run fallback: scan consumed artifact paths on disk so
106
107
  // enforceHandoffConsumes can skip blocks for files that already exist.
@@ -28,6 +28,8 @@ export interface EnforceState {
28
28
  workflow: string;
29
29
  current_role?: string;
30
30
  completed_artifacts?: CompletedArtifactEntry[];
31
+ /** G4: current workflow iteration (for relax_after_iteration rules) */
32
+ iteration?: number;
31
33
  };
32
34
  /** Artifact paths that exist on disk — fallback for re-run idempotency.
33
35
  * CLI checks disk, passes paths here so enforce stays pure (no I/O). */
@@ -23,11 +23,21 @@ import { relative, normalize } from "node:path";
23
23
  * Optional `roles` parameter provides output_schema enforcement.
24
24
  */
25
25
  export function enforcePreToolUse(ir, input, state, roles) {
26
+ // G4 Layer 1: Step enforce rules (state-driven)
27
+ if (state?.workflowState && ir.workflows_ir) {
28
+ const stepRuleResult = enforceStepRules(ir, input, state);
29
+ if (stepRuleResult !== null)
30
+ return stepRuleResult;
31
+ }
26
32
  // Layer 2: Role scope
27
33
  if (input.agent_type && ir.roles_scope_map && ir.roles_scope_map.length > 0) {
28
- const result = enforceRoleScope(ir.roles_scope_map, input);
29
- if (result)
30
- return result;
34
+ // G4: Check if scope is relaxed by iteration-based rule
35
+ const relaxed = isStepScopeRelaxed(ir, state);
36
+ if (!relaxed) {
37
+ const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state));
38
+ if (result)
39
+ return result;
40
+ }
31
41
  }
32
42
  // Layer 3: Tool filters
33
43
  if (ir.tool_filters.length > 0) {
@@ -293,6 +303,53 @@ export function enforceStop(ir, input, workflowState) {
293
303
  return blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
294
304
  messages.map(m => ` - ${m}`).join("\n"));
295
305
  }
306
+ // ── G4: State-driven Step Enforcement ─────────────────────
307
+ /** Write tools that step read_only rule should block */
308
+ const ALL_WRITE_TOOLS = new Set(["Edit", "Write", "NotebookEdit", "Bash"]);
309
+ /**
310
+ * Find the StepEnforceRule for the current step in the active workflow.
311
+ */
312
+ function findStepRule(ir, state) {
313
+ if (!state?.workflowState || !ir.workflows_ir)
314
+ return null;
315
+ const wf = ir.workflows_ir.find(w => w.workflow_name === state.workflowState.workflow);
316
+ if (!wf?.step_enforce_rules)
317
+ return null;
318
+ return wf.step_enforce_rules.find(r => r.step_id === state.workflowState.current_step) ?? null;
319
+ }
320
+ /**
321
+ * G4: Enforce step-level rules.
322
+ * Returns HookOutput if a rule triggers (block for read_only), null to continue.
323
+ */
324
+ function enforceStepRules(ir, input, state) {
325
+ const rule = findStepRule(ir, state);
326
+ if (!rule)
327
+ return null;
328
+ // read_only: block all write tools
329
+ if (rule.read_only && ALL_WRITE_TOOLS.has(input.tool_name)) {
330
+ return blockOutput(`[Intent DNA] Step '${rule.step_id}' is read-only. Write tool '${input.tool_name}' blocked.`);
331
+ }
332
+ return null;
333
+ }
334
+ /**
335
+ * G4: Check if scope enforcement is relaxed for this step based on iteration.
336
+ * When iteration >= relax_after_iteration, scope checks are skipped (rescue mode).
337
+ */
338
+ function isStepScopeRelaxed(ir, state) {
339
+ const rule = findStepRule(ir, state);
340
+ if (!rule?.relax_after_iteration)
341
+ return false;
342
+ const iteration = state?.workflowState?.iteration ?? 1;
343
+ return iteration >= rule.relax_after_iteration;
344
+ }
345
+ /**
346
+ * G4: Get additional write paths granted to the current step.
347
+ * These extend the role's scope for this specific step only.
348
+ */
349
+ function getStepAdditionalPaths(ir, state) {
350
+ const rule = findStepRule(ir, state);
351
+ return rule?.additional_write_paths ?? [];
352
+ }
296
353
  // ── Internal: Layer Enforcement ────────────────────────────
297
354
  /**
298
355
  * Enforce handoff consumes — verify that the current step's consumed
@@ -344,7 +401,7 @@ export function enforceHandoffProduces(ir, wfState) {
344
401
  }
345
402
  return null;
346
403
  }
347
- function enforceRoleScope(rolesScopeMap, input) {
404
+ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
348
405
  if (!input.agent_type)
349
406
  return null;
350
407
  // Standard write tools: check single file path
@@ -352,7 +409,7 @@ function enforceRoleScope(rolesScopeMap, input) {
352
409
  const filePath = extractFilePath(input);
353
410
  if (!filePath)
354
411
  return null;
355
- return checkPathAgainstScope(rolesScopeMap, input, filePath);
412
+ return checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths);
356
413
  }
357
414
  // Bash: extract potential write targets from command
358
415
  if (input.tool_name === "Bash") {
@@ -363,7 +420,7 @@ function enforceRoleScope(rolesScopeMap, input) {
363
420
  if (writePaths.length === 0)
364
421
  return null;
365
422
  for (const p of writePaths) {
366
- const result = checkPathAgainstScope(rolesScopeMap, input, p);
423
+ const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths);
367
424
  if (result)
368
425
  return result;
369
426
  }
@@ -372,7 +429,7 @@ function enforceRoleScope(rolesScopeMap, input) {
372
429
  return null;
373
430
  }
374
431
  /** Check a single file path against role scope. Shared by Write tools and Bash. */
375
- function checkPathAgainstScope(rolesScopeMap, input, filePath) {
432
+ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = []) {
376
433
  const cwd = input.cwd;
377
434
  let relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
378
435
  // Normalize to resolve traversal (e.g., "src/../../test/x.ts" → "../test/x.ts")
@@ -382,11 +439,15 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath) {
382
439
  if (input.agent_type !== agentTypeName)
383
440
  continue;
384
441
  const writeGlobs = entry.scope.write ?? [];
385
- if (writeGlobs.length === 0) {
442
+ // G4: merge step-specific additional_write_paths
443
+ const allWriteGlobs = additionalWritePaths.length > 0
444
+ ? [...writeGlobs, ...additionalWritePaths]
445
+ : writeGlobs;
446
+ if (allWriteGlobs.length === 0) {
386
447
  return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' has no write permission (path: ${filePath}). Merge-time scope gate will filter.`);
387
448
  }
388
- if (!checkWriteAllowed(relativePath, writeGlobs)) {
389
- return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${writeGlobs.join(", ")}). Merge-time scope gate will filter.`);
449
+ if (!checkWriteAllowed(relativePath, allWriteGlobs)) {
450
+ return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${allWriteGlobs.join(", ")}). Merge-time scope gate will filter.`);
390
451
  }
391
452
  return null; // Role matched, write allowed
392
453
  }
@@ -79,19 +79,31 @@ export interface TraceEntry {
79
79
  timestamp: string;
80
80
  }
81
81
  /**
82
- * Append a trace entry to the daily trace file.
83
- * With sessionId: `.dna/state/trace/trace-YYYY-MM-DD-{sessionId}.jsonl`
84
- * Without: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (backward compat)
82
+ * Append a trace entry with dual-write strategy:
83
+ * 1. Always → global trace: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (merged view)
84
+ * 2. If sessionId → session-local: `.dna/state/sessions/{id}/trace.jsonl`
85
+ *
86
+ * G1: Session isolation — session traces live in the session directory,
87
+ * global trace provides a unified view across all sessions.
85
88
  * Fail-open: never throws.
86
89
  */
87
90
  export declare function appendTrace(projectDir: string, entry: TraceEntry, sessionId?: string): Promise<void>;
88
91
  /**
89
- * Read trace entries from the last N days.
90
- * With sessionId: reads only that session's trace files.
91
- * Without: reads all trace files (merged view).
92
+ * Read trace entries.
93
+ *
94
+ * G1 session isolation:
95
+ * - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
96
+ * (falls back to legacy global files with sessionId in name for backward compat)
97
+ * - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
98
+ *
92
99
  * Returns parsed entries sorted by timestamp.
93
100
  */
94
101
  export declare function readTraces(projectDir: string, days?: number, sessionId?: string): Promise<TraceEntry[]>;
102
+ /**
103
+ * List active session directories under `.dna/state/sessions/`.
104
+ * Returns session IDs (directory names).
105
+ */
106
+ export declare function listSessions(projectDir: string): Promise<string[]>;
95
107
  /**
96
108
  * Clean up trace files older than retention period.
97
109
  * Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
@@ -140,48 +140,104 @@ const TRACE_DIR = "trace";
140
140
  const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
141
141
  const TRACE_RETENTION_DAYS = 7;
142
142
  /**
143
- * Build trace file name.
144
- * With session isolation: `trace-{date}-{sessionId}.jsonl`
145
- * Without: `trace-{date}.jsonl` (backward compat / merged view)
143
+ * Build global trace file name (merged view, no sessionId).
144
+ * Legacy files with sessionId in name are still readable for backward compat.
146
145
  */
147
- function traceFileName(date, sessionId) {
148
- if (sessionId) {
149
- return `trace-${date}-${sessionId}.jsonl`;
150
- }
146
+ function globalTraceFileName(date) {
151
147
  return `trace-${date}.jsonl`;
152
148
  }
153
149
  /**
154
- * Append a trace entry to the daily trace file.
155
- * With sessionId: `.dna/state/trace/trace-YYYY-MM-DD-{sessionId}.jsonl`
156
- * Without: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (backward compat)
150
+ * Append a trace entry with dual-write strategy:
151
+ * 1. Always → global trace: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (merged view)
152
+ * 2. If sessionId → session-local: `.dna/state/sessions/{id}/trace.jsonl`
153
+ *
154
+ * G1: Session isolation — session traces live in the session directory,
155
+ * global trace provides a unified view across all sessions.
157
156
  * Fail-open: never throws.
158
157
  */
159
158
  export async function appendTrace(projectDir, entry, sessionId) {
160
159
  try {
161
- const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
162
- await mkdir(traceDir, { recursive: true });
160
+ const line = JSON.stringify(entry) + "\n";
163
161
  const date = entry.timestamp.slice(0, 10);
164
- const tracePath = join(traceDir, traceFileName(date, sessionId));
165
- // Check file size rotate if over limit
162
+ // 1. Global trace (merged view)
163
+ const globalTraceDir = join(projectDir, ".dna", "state", TRACE_DIR);
164
+ await mkdir(globalTraceDir, { recursive: true });
165
+ const globalPath = join(globalTraceDir, globalTraceFileName(date));
166
166
  try {
167
- const stats = await stat(tracePath);
167
+ const stats = await stat(globalPath);
168
168
  if (stats.size >= MAX_TRACE_SIZE)
169
- return; // silently skip if file too large
169
+ return;
170
170
  }
171
171
  catch { /* file doesn't exist yet */ }
172
- await appendFile(tracePath, JSON.stringify(entry) + "\n", "utf-8");
172
+ await appendFile(globalPath, line, "utf-8");
173
+ // 2. Session-local trace (if session isolated)
174
+ if (sessionId) {
175
+ const sessionDir = resolveStateDir(projectDir, sessionId);
176
+ await mkdir(sessionDir, { recursive: true });
177
+ const sessionTracePath = join(sessionDir, "trace.jsonl");
178
+ try {
179
+ const stats = await stat(sessionTracePath);
180
+ if (stats.size >= MAX_TRACE_SIZE)
181
+ return;
182
+ }
183
+ catch { /* file doesn't exist yet */ }
184
+ await appendFile(sessionTracePath, line, "utf-8");
185
+ }
173
186
  }
174
187
  catch {
175
188
  // Fail-open: trace write failure never affects hook execution
176
189
  }
177
190
  }
178
191
  /**
179
- * Read trace entries from the last N days.
180
- * With sessionId: reads only that session's trace files.
181
- * Without: reads all trace files (merged view).
192
+ * Read trace entries.
193
+ *
194
+ * G1 session isolation:
195
+ * - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
196
+ * (falls back to legacy global files with sessionId in name for backward compat)
197
+ * - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
198
+ *
182
199
  * Returns parsed entries sorted by timestamp.
183
200
  */
184
201
  export async function readTraces(projectDir, days = 1, sessionId) {
202
+ const entries = [];
203
+ if (sessionId) {
204
+ // Session-specific: read from session directory first
205
+ const sessionDir = resolveStateDir(projectDir, sessionId);
206
+ const sessionTracePath = join(sessionDir, "trace.jsonl");
207
+ const sessionEntries = await readTraceFile(sessionTracePath);
208
+ entries.push(...sessionEntries);
209
+ // Backward compat: also check legacy files in global trace dir
210
+ if (entries.length === 0) {
211
+ const legacyEntries = await readGlobalTraces(projectDir, days, sessionId);
212
+ entries.push(...legacyEntries);
213
+ }
214
+ }
215
+ else {
216
+ // Global merged view
217
+ const globalEntries = await readGlobalTraces(projectDir, days);
218
+ entries.push(...globalEntries);
219
+ }
220
+ return entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
221
+ }
222
+ /** Read all entries from a single trace file. */
223
+ async function readTraceFile(filePath) {
224
+ const entries = [];
225
+ try {
226
+ const content = await readFile(filePath, "utf-8");
227
+ for (const line of content.trim().split("\n")) {
228
+ if (!line)
229
+ continue;
230
+ try {
231
+ entries.push(JSON.parse(line));
232
+ }
233
+ catch { /* skip malformed */ }
234
+ }
235
+ }
236
+ catch { /* file doesn't exist */ }
237
+ return entries;
238
+ }
239
+ /** Read global trace files from `.dna/state/trace/`. */
240
+ async function readGlobalTraces(projectDir, days, sessionIdFilter) {
185
241
  const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
186
242
  const entries = [];
187
243
  const cutoff = new Date();
@@ -192,28 +248,20 @@ export async function readTraces(projectDir, days = 1, sessionId) {
192
248
  const traceFiles = files
193
249
  .filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"))
194
250
  .filter(f => {
195
- // Extract date from filename: trace-YYYY-MM-DD.jsonl or trace-YYYY-MM-DD-{sessionId}.jsonl
196
251
  const fileDate = f.slice(6, 16); // "trace-YYYY-MM-DD..."
197
252
  if (fileDate < cutoffDate)
198
253
  return false;
199
- // Session filter: only include files for this session (or shared files)
200
- if (sessionId) {
201
- const suffix = f.slice(16); // "-{sessionId}.jsonl" or ".jsonl"
202
- return suffix === `-${sessionId}.jsonl` || suffix === ".jsonl";
254
+ // Legacy session filter: files with sessionId in name
255
+ if (sessionIdFilter) {
256
+ const suffix = f.slice(16);
257
+ return suffix === `-${sessionIdFilter}.jsonl` || suffix === ".jsonl";
203
258
  }
204
259
  return true;
205
260
  })
206
261
  .sort();
207
262
  for (const file of traceFiles) {
208
- const content = await readFile(join(traceDir, file), "utf-8");
209
- for (const line of content.trim().split("\n")) {
210
- if (!line)
211
- continue;
212
- try {
213
- entries.push(JSON.parse(line));
214
- }
215
- catch { /* skip malformed */ }
216
- }
263
+ const fileEntries = await readTraceFile(join(traceDir, file));
264
+ entries.push(...fileEntries);
217
265
  }
218
266
  }
219
267
  catch {
@@ -221,6 +269,29 @@ export async function readTraces(projectDir, days = 1, sessionId) {
221
269
  }
222
270
  return entries;
223
271
  }
272
+ /**
273
+ * List active session directories under `.dna/state/sessions/`.
274
+ * Returns session IDs (directory names).
275
+ */
276
+ export async function listSessions(projectDir) {
277
+ const sessionsDir = join(projectDir, ".dna", "state", "sessions");
278
+ try {
279
+ const entries = await readdir(sessionsDir);
280
+ const sessions = [];
281
+ for (const entry of entries) {
282
+ try {
283
+ const entryStat = await stat(join(sessionsDir, entry));
284
+ if (entryStat.isDirectory())
285
+ sessions.push(entry);
286
+ }
287
+ catch { /* skip */ }
288
+ }
289
+ return sessions;
290
+ }
291
+ catch {
292
+ return [];
293
+ }
294
+ }
224
295
  /**
225
296
  * Clean up trace files older than retention period.
226
297
  * Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Intent DNA — MCP Server (`dna-mcp`)
4
+ *
5
+ * Claude Code MCP server providing DNA governance tools:
6
+ * G2: State management (workflow, trace, status)
7
+ * G3: Compile toolchain (compile, validate, sync)
8
+ *
9
+ * Usage: dna-mcp [--project-dir <path>]
10
+ *
11
+ * Speaks JSON-RPC 2.0 over stdio (MCP protocol).
12
+ */
13
+ export {};