intentdna 1.5.2 → 1.5.4

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.4",
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.4",
4
4
  "description": "Declarative policy layer for AI agent governance"
5
5
  }
@@ -54,4 +54,27 @@ export { getPackageVersion } from "../util/version.js";
54
54
  * Throws if two configs declare the same namespace.
55
55
  */
56
56
  export declare function checkNamespaceCollisions(configPaths: string[]): Promise<void>;
57
+ /**
58
+ * Compare two semver strings. Returns:
59
+ * 1 if a > b
60
+ * 0 if a == b
61
+ * -1 if a < b
62
+ * Handles major.minor.patch format. Non-numeric parts are ignored.
63
+ */
64
+ export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
65
+ /** Info about a single template that has an available upgrade. */
66
+ export interface TemplateUpgradeInfo {
67
+ configPath: string;
68
+ templateName: string;
69
+ currentVersion: string;
70
+ availableVersion: string;
71
+ }
72
+ /**
73
+ * Check template versions against current package version.
74
+ * Returns upgrade info for outdated configs and prints human-readable suggestions.
75
+ *
76
+ * U1: Semver-aware comparison — only suggests upgrades when package is strictly newer.
77
+ * Handles missing _template_version (old configs created before versioning).
78
+ */
79
+ export declare function checkTemplateVersions(configPaths: string[]): Promise<TemplateUpgradeInfo[]>;
57
80
  export declare function runSync(opts: SyncOptions): Promise<number>;
@@ -253,28 +253,72 @@ export async function checkNamespaceCollisions(configPaths) {
253
253
  }
254
254
  }
255
255
  }
256
+ /**
257
+ * Compare two semver strings. Returns:
258
+ * 1 if a > b
259
+ * 0 if a == b
260
+ * -1 if a < b
261
+ * Handles major.minor.patch format. Non-numeric parts are ignored.
262
+ */
263
+ export function compareSemver(a, b) {
264
+ const parse = (v) => v.split(".").map(s => parseInt(s, 10) || 0);
265
+ const pa = parse(a);
266
+ const pb = parse(b);
267
+ const len = Math.max(pa.length, pb.length);
268
+ for (let i = 0; i < len; i++) {
269
+ const va = pa[i] ?? 0;
270
+ const vb = pb[i] ?? 0;
271
+ if (va > vb)
272
+ return 1;
273
+ if (va < vb)
274
+ return -1;
275
+ }
276
+ return 0;
277
+ }
256
278
  /**
257
279
  * Check template versions against current package version.
258
- * Prints upgrade suggestions for outdated configs.
280
+ * Returns upgrade info for outdated configs and prints human-readable suggestions.
281
+ *
282
+ * U1: Semver-aware comparison — only suggests upgrades when package is strictly newer.
283
+ * Handles missing _template_version (old configs created before versioning).
259
284
  */
260
- async function checkTemplateVersions(configPaths) {
285
+ export async function checkTemplateVersions(configPaths) {
261
286
  const pkgVersion = await getPackageVersion();
262
287
  if (!pkgVersion)
263
- return;
288
+ return [];
289
+ const upgrades = [];
264
290
  for (const configPath of configPaths) {
265
291
  try {
266
292
  const content = await readFile(configPath, "utf-8");
267
293
  const data = parseYAML(content);
268
294
  const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
269
295
  const tmplName = typeof data._source_template === "string" ? data._source_template : undefined;
270
- if (tmplVersion && tmplName && tmplVersion !== pkgVersion) {
271
- process.stderr.write(`Template "${tmplName}" has update: ${tmplVersion} ${pkgVersion}. Run: dna init --upgrade ${tmplName}\n`);
296
+ if (tmplName && !tmplVersion) {
297
+ // Old config without version tracking suggest upgrade
298
+ upgrades.push({
299
+ configPath,
300
+ templateName: tmplName,
301
+ currentVersion: "unknown",
302
+ availableVersion: pkgVersion,
303
+ });
304
+ process.stderr.write(`Upgrade available: "${tmplName}" has no version tag. Run: dna init --upgrade ${tmplName}\n`);
305
+ }
306
+ else if (tmplVersion && tmplName && compareSemver(pkgVersion, tmplVersion) > 0) {
307
+ // Package is strictly newer than template — suggest upgrade
308
+ upgrades.push({
309
+ configPath,
310
+ templateName: tmplName,
311
+ currentVersion: tmplVersion,
312
+ availableVersion: pkgVersion,
313
+ });
314
+ process.stderr.write(`Upgrade available: "${tmplName}" ${tmplVersion} → ${pkgVersion}. Run: dna init --upgrade ${tmplName}\n`);
272
315
  }
273
316
  }
274
317
  catch {
275
318
  // Non-critical — skip version check for unparseable files
276
319
  }
277
320
  }
321
+ return upgrades;
278
322
  }
279
323
  function resolveSpeciesPath(ref) {
280
324
  if (!ref.startsWith("species:"))
@@ -483,6 +527,8 @@ export async function runSync(opts) {
483
527
  if (removed > 0) {
484
528
  process.stderr.write(`Cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
485
529
  }
530
+ // G2/G3: Register built-in DNA MCP server in .claude/.mcp.json
531
+ await registerDNAMCPServer(cwd);
486
532
  }
487
533
  // Step 3.6: Register hooks in settings.json (bin mode only)
488
534
  // Plugin mode: hooks managed by plugin framework — skip settings.json
@@ -680,3 +726,28 @@ async function writeMCPConfig(mcpDeps, mcpJsonPath, variables) {
680
726
  function substituteVariables(template, vars) {
681
727
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
682
728
  }
729
+ /**
730
+ * G2/G3: Register the built-in DNA MCP server (dna-mcp) in .claude/.mcp.json.
731
+ * Idempotent: only adds if not already present.
732
+ */
733
+ async function registerDNAMCPServer(cwd) {
734
+ const mcpJsonPath = resolve(cwd, ".claude", ".mcp.json");
735
+ let existing = {};
736
+ try {
737
+ const raw = await readFile(mcpJsonPath, "utf-8");
738
+ existing = JSON.parse(raw);
739
+ }
740
+ catch { /* doesn't exist yet */ }
741
+ const mcpServers = (existing.mcpServers ?? {});
742
+ // Don't overwrite if user already configured it
743
+ if (mcpServers["intentdna"])
744
+ return;
745
+ mcpServers["intentdna"] = {
746
+ command: "dna-mcp",
747
+ args: ["--project-dir", cwd],
748
+ };
749
+ existing.mcpServers = mcpServers;
750
+ await mkdir(dirname(mcpJsonPath), { recursive: true });
751
+ await writeFileAsync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
752
+ process.stderr.write(`MCP: registered dna-mcp server in ${mcpJsonPath}\n`);
753
+ }
@@ -12,6 +12,7 @@
12
12
  export interface VerifyOptions {
13
13
  lockFile: string;
14
14
  stats?: boolean;
15
+ health?: boolean;
15
16
  }
16
17
  export interface LockFileEntry {
17
18
  sha256: string;
@@ -31,3 +32,13 @@ export declare function sha256(content: string): string;
31
32
  export declare function parseLockFile(raw: string): LockFile;
32
33
  export declare function verifyLock(lock: LockFile): Promise<VerifyResult[]>;
33
34
  export declare function runVerify(opts: VerifyOptions): Promise<number>;
35
+ export interface HealthCheckResult {
36
+ name: string;
37
+ status: "pass" | "warn" | "fail";
38
+ detail: string;
39
+ }
40
+ /**
41
+ * U3: Comprehensive health report for DNA installation.
42
+ * Checks IR, configs, templates, hooks, and trace system.
43
+ */
44
+ export declare function runHealth(): Promise<number>;
@@ -54,6 +54,10 @@ export async function verifyLock(lock) {
54
54
  }
55
55
  // ── CLI entry ──────────────────────────────────────────────
56
56
  export async function runVerify(opts) {
57
+ // Handle --health mode
58
+ if (opts.health) {
59
+ return runHealth();
60
+ }
57
61
  // Handle --stats mode
58
62
  if (opts.stats) {
59
63
  return runStats();
@@ -214,3 +218,189 @@ function truncate(s, max) {
214
218
  return s;
215
219
  return s.slice(0, max - 3) + "...";
216
220
  }
221
+ /**
222
+ * U3: Comprehensive health report for DNA installation.
223
+ * Checks IR, configs, templates, hooks, and trace system.
224
+ */
225
+ export async function runHealth() {
226
+ const cwd = process.cwd();
227
+ const checks = [];
228
+ const { resolve } = await import("node:path");
229
+ const { stat, readFile: rf, readdir } = await import("node:fs/promises");
230
+ // 1. Check compiled IR
231
+ const irPath = resolve(cwd, ".dna", "compiled", "ir.json");
232
+ try {
233
+ const raw = await rf(irPath, "utf-8");
234
+ const data = JSON.parse(raw);
235
+ if (data.ir_version && data.ir) {
236
+ const age = Date.now() - new Date(data.compiled_at ?? data.ir.compiled_at).getTime();
237
+ const ageHours = Math.round(age / 3600000);
238
+ checks.push({
239
+ name: "Compiled IR",
240
+ status: "pass",
241
+ detail: `v${data.ir_version}, compiled ${ageHours}h ago, ${data.ir.source_dna_ids?.length ?? 0} template(s)`,
242
+ });
243
+ }
244
+ else if (data.compiled_at) {
245
+ checks.push({ name: "Compiled IR", status: "warn", detail: "Legacy format — run `dna sync` to upgrade" });
246
+ }
247
+ else {
248
+ checks.push({ name: "Compiled IR", status: "fail", detail: "Unrecognized format" });
249
+ }
250
+ }
251
+ catch {
252
+ checks.push({ name: "Compiled IR", status: "fail", detail: "Not found — run `dna sync`" });
253
+ }
254
+ // 2. Check DNA config files
255
+ const { autoDetectConfigs } = await import("./sync.js");
256
+ const configs = await autoDetectConfigs();
257
+ if (configs.length > 0) {
258
+ checks.push({ name: "DNA configs", status: "pass", detail: `${configs.length} config(s) found` });
259
+ }
260
+ else {
261
+ checks.push({ name: "DNA configs", status: "fail", detail: "No config files found — run `dna init --template <name>`" });
262
+ }
263
+ // 3. Check config validity
264
+ if (configs.length > 0) {
265
+ const { loadDNA } = await import("../../compiler/index.js");
266
+ let valid = 0;
267
+ let invalid = 0;
268
+ const errors = [];
269
+ for (const configPath of configs) {
270
+ try {
271
+ await loadDNA(configPath);
272
+ valid++;
273
+ }
274
+ catch (err) {
275
+ invalid++;
276
+ const { basename } = await import("node:path");
277
+ errors.push(`${basename(configPath)}: ${err instanceof Error ? err.message : String(err)}`);
278
+ }
279
+ }
280
+ if (invalid === 0) {
281
+ checks.push({ name: "Config validity", status: "pass", detail: `${valid} config(s) valid` });
282
+ }
283
+ else {
284
+ checks.push({ name: "Config validity", status: "fail", detail: errors.join("; ") });
285
+ }
286
+ }
287
+ // 4. Check template versions
288
+ if (configs.length > 0) {
289
+ const { checkTemplateVersions: checkVersions } = await import("./sync.js");
290
+ // Suppress stderr output from checkTemplateVersions — we'll report ourselves
291
+ const origWrite = process.stderr.write;
292
+ process.stderr.write = (() => true);
293
+ try {
294
+ const upgrades = await checkVersions(configs);
295
+ if (upgrades.length === 0) {
296
+ checks.push({ name: "Template versions", status: "pass", detail: "All up to date" });
297
+ }
298
+ else {
299
+ const names = upgrades.map(u => `${u.templateName} (${u.currentVersion} → ${u.availableVersion})`);
300
+ checks.push({ name: "Template versions", status: "warn", detail: `Upgrade available: ${names.join(", ")}` });
301
+ }
302
+ }
303
+ catch {
304
+ checks.push({ name: "Template versions", status: "warn", detail: "Could not check versions" });
305
+ }
306
+ finally {
307
+ process.stderr.write = origWrite;
308
+ }
309
+ }
310
+ // 5. Check .claude settings or plugin registration
311
+ const settingsPath = resolve(cwd, ".claude", "settings.json");
312
+ try {
313
+ const raw = await rf(settingsPath, "utf-8");
314
+ const settings = JSON.parse(raw);
315
+ const hooks = settings.hooks;
316
+ const hasHooks = hooks && typeof hooks === "object" && Object.keys(hooks).length > 0;
317
+ if (hasHooks) {
318
+ const eventCount = Object.keys(hooks).length;
319
+ checks.push({ name: "Hook registration", status: "pass", detail: `${eventCount} event(s) in settings.json` });
320
+ }
321
+ else {
322
+ checks.push({ name: "Hook registration", status: "warn", detail: "No hooks in settings.json — using plugin mode?" });
323
+ }
324
+ }
325
+ catch {
326
+ // Check for plugin registration instead
327
+ const mcpPath = resolve(cwd, ".claude", ".mcp.json");
328
+ try {
329
+ const raw = await rf(mcpPath, "utf-8");
330
+ const mcp = JSON.parse(raw);
331
+ if (mcp.mcpServers?.intentdna) {
332
+ checks.push({ name: "MCP server", status: "pass", detail: "dna-mcp registered in .mcp.json" });
333
+ }
334
+ else {
335
+ checks.push({ name: "Hook registration", status: "warn", detail: "No hooks or MCP server found" });
336
+ }
337
+ }
338
+ catch {
339
+ checks.push({ name: "Hook registration", status: "warn", detail: "No settings.json or .mcp.json — run `dna setup` or `dna sync`" });
340
+ }
341
+ }
342
+ // 6. Check trace system
343
+ const traceDir = resolve(cwd, ".dna", "state", "trace");
344
+ try {
345
+ const files = await readdir(traceDir);
346
+ const traceFiles = files.filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"));
347
+ if (traceFiles.length > 0) {
348
+ // Read latest trace file size
349
+ const latest = traceFiles.sort().pop();
350
+ const latestStat = await stat(resolve(traceDir, latest));
351
+ const sizeKB = Math.round(latestStat.size / 1024);
352
+ checks.push({ name: "Trace system", status: "pass", detail: `${traceFiles.length} file(s), latest ${sizeKB}KB` });
353
+ }
354
+ else {
355
+ checks.push({ name: "Trace system", status: "warn", detail: "No trace files — hooks not fired yet" });
356
+ }
357
+ }
358
+ catch {
359
+ checks.push({ name: "Trace system", status: "warn", detail: "No trace directory — will be created on first hook call" });
360
+ }
361
+ // 7. Check lock file drift
362
+ const lockPath = resolve(cwd, ".dna", "lock");
363
+ try {
364
+ const raw = await rf(lockPath, "utf-8");
365
+ const lock = parseLockFile(raw);
366
+ const results = await verifyLock(lock);
367
+ const drifted = results.filter(r => r.status !== "match");
368
+ if (drifted.length === 0) {
369
+ checks.push({ name: "Output drift", status: "pass", detail: `${results.length} file(s) match lock` });
370
+ }
371
+ else {
372
+ checks.push({ name: "Output drift", status: "warn", detail: `${drifted.length}/${results.length} file(s) drifted — run \`dna sync\`` });
373
+ }
374
+ }
375
+ catch {
376
+ checks.push({ name: "Output drift", status: "warn", detail: "No lock file — run `dna sync` to generate" });
377
+ }
378
+ // ── Format output ──────────────────────────────────────
379
+ const ICONS = { pass: "ok", warn: "!!", fail: "XX" };
380
+ process.stderr.write("\nIntent DNA Health Report\n");
381
+ process.stderr.write("========================\n\n");
382
+ let hasFailure = false;
383
+ let hasWarning = false;
384
+ for (const check of checks) {
385
+ const icon = ICONS[check.status];
386
+ const label = check.name.padEnd(20);
387
+ process.stderr.write(` [${icon}] ${label} ${check.detail}\n`);
388
+ if (check.status === "fail")
389
+ hasFailure = true;
390
+ if (check.status === "warn")
391
+ hasWarning = true;
392
+ }
393
+ process.stderr.write("\n");
394
+ if (hasFailure) {
395
+ process.stderr.write("Status: issues found — see [XX] items above\n");
396
+ return 1;
397
+ }
398
+ else if (hasWarning) {
399
+ process.stderr.write("Status: healthy with warnings — see [!!] items above\n");
400
+ return 0;
401
+ }
402
+ else {
403
+ process.stderr.write("Status: all checks passed\n");
404
+ return 0;
405
+ }
406
+ }
package/dist/cli/index.js CHANGED
@@ -58,6 +58,7 @@ Examples:
58
58
  dna verify Verify synced files against .dna/lock
59
59
  dna verify --lock /path/to/.dna/lock
60
60
  dna verify --stats Show hook call statistics (last 24h)
61
+ dna verify --health Comprehensive health check of DNA installation
61
62
  dna import . Import existing harness configs into DNA format
62
63
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8
63
64
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8 --dry-run
@@ -126,6 +127,7 @@ async function main() {
126
127
  options: {
127
128
  lock: { type: "string", default: ".dna/lock" },
128
129
  stats: { type: "boolean", default: false },
130
+ health: { type: "boolean", default: false },
129
131
  },
130
132
  allowPositionals: true,
131
133
  strict: false,
@@ -134,6 +136,7 @@ async function main() {
134
136
  const code = await runVerify({
135
137
  lockFile: verifyValues.lock,
136
138
  stats: verifyValues.stats,
139
+ health: verifyValues.health,
137
140
  });
138
141
  process.exit(code);
139
142
  break;
@@ -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.
@@ -277,7 +278,8 @@ async function loadIR(irPath) {
277
278
  // Support both raw IR and wrapped CompiledIRFile format
278
279
  if (data.ir_version && data.ir) {
279
280
  if (data.ir_version !== EXPECTED_IR_VERSION) {
280
- process.stderr.write(`[Intent DNA] IR version mismatch: file has v${data.ir_version}, expected v${EXPECTED_IR_VERSION}. Run \`dna sync\` to recompile.\n`);
281
+ process.stderr.write(`\n Intent DNA: IR version mismatch (v${data.ir_version} on disk, v${EXPECTED_IR_VERSION} expected)\n` +
282
+ ` Fix: run \`dna sync\` to recompile\n\n`);
281
283
  }
282
284
  return data.ir;
283
285
  }
@@ -285,9 +287,21 @@ async function loadIR(irPath) {
285
287
  if (data.compiled_at && data.source_dna_ids) {
286
288
  return data;
287
289
  }
290
+ process.stderr.write(`\n Intent DNA: unrecognized IR format at ${irPath}\n` +
291
+ ` Fix: run \`dna sync\` to regenerate\n\n`);
288
292
  return null;
289
293
  }
290
- catch {
294
+ catch (err) {
295
+ // U3: Human-readable error instead of silent failure
296
+ const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
297
+ if (isNotFound) {
298
+ process.stderr.write(`\n Intent DNA: no compiled IR found at ${irPath}\n` +
299
+ ` Fix: run \`dna sync\` to compile your DNA config\n\n`);
300
+ }
301
+ else {
302
+ process.stderr.write(`\n Intent DNA: failed to load IR — ${err instanceof Error ? err.message : String(err)}\n` +
303
+ ` Fix: run \`dna sync\` to recompile\n\n`);
304
+ }
291
305
  return null;
292
306
  }
293
307
  }
@@ -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). */