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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/sync.js +27 -0
- package/dist/compiler/compile.js +11 -0
- package/dist/governance/index.d.ts +7 -0
- package/dist/governance/index.js +7 -0
- package/dist/governance/types.d.ts +135 -0
- package/dist/governance/types.js +12 -0
- package/dist/hooks/cli.js +1 -0
- package/dist/hooks/enforce.d.ts +2 -0
- package/dist/hooks/enforce.js +71 -10
- package/dist/hooks/state.d.ts +18 -6
- package/dist/hooks/state.js +105 -34
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +38 -0
- package/dist/mcp/server.d.ts +39 -0
- package/dist/mcp/server.js +84 -0
- package/dist/mcp/tools-compile.d.ts +10 -0
- package/dist/mcp/tools-compile.js +214 -0
- package/dist/mcp/tools-state.d.ts +11 -0
- package/dist/mcp/tools-state.js +160 -0
- package/dist/mcp/transport.d.ts +36 -0
- package/dist/mcp/transport.js +48 -0
- package/dist/schema/types.d.ts +16 -0
- package/package.json +3 -2
- package/spec/foundation-hardening.md +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
|
+
}
|
package/dist/compiler/compile.js
CHANGED
|
@@ -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,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.
|
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -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). */
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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,
|
|
389
|
-
return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${
|
|
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
|
}
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -79,19 +79,31 @@ export interface TraceEntry {
|
|
|
79
79
|
timestamp: string;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Append a trace entry
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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.
|
package/dist/hooks/state.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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
|
|
162
|
-
await mkdir(traceDir, { recursive: true });
|
|
160
|
+
const line = JSON.stringify(entry) + "\n";
|
|
163
161
|
const date = entry.timestamp.slice(0, 10);
|
|
164
|
-
|
|
165
|
-
|
|
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(
|
|
167
|
+
const stats = await stat(globalPath);
|
|
168
168
|
if (stats.size >= MAX_TRACE_SIZE)
|
|
169
|
-
return;
|
|
169
|
+
return;
|
|
170
170
|
}
|
|
171
171
|
catch { /* file doesn't exist yet */ }
|
|
172
|
-
await appendFile(
|
|
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
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
-
//
|
|
200
|
-
if (
|
|
201
|
-
const suffix = f.slice(16);
|
|
202
|
-
return suffix === `-${
|
|
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
|
|
209
|
-
|
|
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 {};
|