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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/sync.d.ts +23 -0
- package/dist/cli/commands/sync.js +76 -5
- package/dist/cli/commands/verify.d.ts +11 -0
- package/dist/cli/commands/verify.js +190 -0
- package/dist/cli/index.js +3 -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 +16 -2
- 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 +40 -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-enforce.d.ts +27 -0
- package/dist/mcp/tools-enforce.js +178 -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 +8 -8
|
@@ -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
|
-
*
|
|
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 (
|
|
271
|
-
|
|
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;
|
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.
|
|
@@ -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(
|
|
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
|
}
|
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). */
|