intentdna 1.4.5 → 1.4.7
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/feedback.d.ts +33 -0
- package/dist/cli/commands/feedback.js +140 -0
- package/dist/cli/commands/generate.js +3 -0
- package/dist/cli/commands/setup.d.ts +1 -0
- package/dist/cli/commands/setup.js +125 -0
- package/dist/cli/index.js +21 -1
- package/dist/hooks/cli.d.ts +20 -1
- package/dist/hooks/cli.js +61 -5
- package/dist/hooks/enforce.js +5 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/runtime/skill-adapter.js +8 -0
- package/dist/templates/incident-response.dna.yaml +66 -0
- package/dist/templates/migration-safety.dna.yaml +67 -0
- package/dist/templates/performance-audit.dna.yaml +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dna feedback [--days <N>] [--json]
|
|
3
|
+
*
|
|
4
|
+
* Analyze trace/audit data and produce actionable feedback for template optimization.
|
|
5
|
+
*
|
|
6
|
+
* Reads: .dna/state/trace/trace-*.jsonl
|
|
7
|
+
* Outputs: human-readable report (or JSON with --json)
|
|
8
|
+
*/
|
|
9
|
+
export interface FeedbackOptions {
|
|
10
|
+
days: number;
|
|
11
|
+
json: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface FeedbackReport {
|
|
14
|
+
period_days: number;
|
|
15
|
+
total_events: number;
|
|
16
|
+
blocks: number;
|
|
17
|
+
warns: number;
|
|
18
|
+
allows: number;
|
|
19
|
+
top_blocked_tools: Array<{
|
|
20
|
+
tool: string;
|
|
21
|
+
count: number;
|
|
22
|
+
}>;
|
|
23
|
+
top_blocked_paths: Array<{
|
|
24
|
+
path: string;
|
|
25
|
+
count: number;
|
|
26
|
+
}>;
|
|
27
|
+
top_block_reasons: Array<{
|
|
28
|
+
reason: string;
|
|
29
|
+
count: number;
|
|
30
|
+
}>;
|
|
31
|
+
suggestions: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function runFeedback(opts: FeedbackOptions): Promise<number>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dna feedback [--days <N>] [--json]
|
|
3
|
+
*
|
|
4
|
+
* Analyze trace/audit data and produce actionable feedback for template optimization.
|
|
5
|
+
*
|
|
6
|
+
* Reads: .dna/state/trace/trace-*.jsonl
|
|
7
|
+
* Outputs: human-readable report (or JSON with --json)
|
|
8
|
+
*/
|
|
9
|
+
import { readTraces } from "../../hooks/state.js";
|
|
10
|
+
function aggregate(traces) {
|
|
11
|
+
let blocks = 0, warns = 0, allows = 0;
|
|
12
|
+
const toolCounts = new Map();
|
|
13
|
+
const pathCounts = new Map();
|
|
14
|
+
const reasonCounts = new Map();
|
|
15
|
+
for (const t of traces) {
|
|
16
|
+
if (t.decision === "block") {
|
|
17
|
+
blocks++;
|
|
18
|
+
if (t.tool_name)
|
|
19
|
+
toolCounts.set(t.tool_name, (toolCounts.get(t.tool_name) ?? 0) + 1);
|
|
20
|
+
if (t.target_path)
|
|
21
|
+
pathCounts.set(t.target_path, (pathCounts.get(t.target_path) ?? 0) + 1);
|
|
22
|
+
if (t.reason) {
|
|
23
|
+
// Normalize reason to first line
|
|
24
|
+
const shortReason = t.reason.split("\n")[0].replace(/^\[Intent DNA\]\s*/, "").slice(0, 100);
|
|
25
|
+
reasonCounts.set(shortReason, (reasonCounts.get(shortReason) ?? 0) + 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (t.decision === "warn") {
|
|
29
|
+
warns++;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
allows++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const sorted = (map, limit, key) => [...map.entries()]
|
|
36
|
+
.sort((a, b) => b[1] - a[1])
|
|
37
|
+
.slice(0, limit)
|
|
38
|
+
.map(([k, count]) => ({ [key]: k, count }));
|
|
39
|
+
return {
|
|
40
|
+
total_events: traces.length,
|
|
41
|
+
blocks,
|
|
42
|
+
warns,
|
|
43
|
+
allows,
|
|
44
|
+
top_blocked_tools: sorted(toolCounts, 5, "tool"),
|
|
45
|
+
top_blocked_paths: sorted(pathCounts, 5, "path"),
|
|
46
|
+
top_block_reasons: sorted(reasonCounts, 5, "reason"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function generateSuggestions(report) {
|
|
50
|
+
const suggestions = [];
|
|
51
|
+
if (report.blocks === 0 && report.warns === 0) {
|
|
52
|
+
suggestions.push("No blocks or warnings — DNA constraints are not firing. Consider tightening rules or verifying hooks are active.");
|
|
53
|
+
return suggestions;
|
|
54
|
+
}
|
|
55
|
+
const blockRate = report.total_events > 0 ? report.blocks / report.total_events : 0;
|
|
56
|
+
if (blockRate > 0.3) {
|
|
57
|
+
suggestions.push(`High block rate (${(blockRate * 100).toFixed(0)}%). Review if DNA rules are too restrictive — this may slow development.`);
|
|
58
|
+
}
|
|
59
|
+
// Tool-specific suggestions
|
|
60
|
+
for (const t of report.top_blocked_tools) {
|
|
61
|
+
if (t.count >= 5) {
|
|
62
|
+
suggestions.push(`Tool '${t.tool}' blocked ${t.count} times. Consider expanding permissions if this is expected usage.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Path-specific suggestions
|
|
66
|
+
for (const p of report.top_blocked_paths) {
|
|
67
|
+
if (p.count >= 3) {
|
|
68
|
+
suggestions.push(`Path '${p.path}' blocked ${p.count} times. Consider adding to write scope if agents need access.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Repeated reason patterns
|
|
72
|
+
for (const r of report.top_block_reasons) {
|
|
73
|
+
if (r.count >= 3) {
|
|
74
|
+
suggestions.push(`Repeated block: "${r.reason}" (${r.count}x). May indicate a rule-scope mismatch.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (suggestions.length === 0) {
|
|
78
|
+
suggestions.push("Constraints are working within normal parameters. No action needed.");
|
|
79
|
+
}
|
|
80
|
+
return suggestions;
|
|
81
|
+
}
|
|
82
|
+
function formatReport(report) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push(`DNA Feedback Report (last ${report.period_days} day${report.period_days > 1 ? "s" : ""})`);
|
|
85
|
+
lines.push("=".repeat(50));
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push(`Total events: ${report.total_events}`);
|
|
88
|
+
lines.push(` Allowed: ${report.allows}`);
|
|
89
|
+
lines.push(` Blocked: ${report.blocks}`);
|
|
90
|
+
lines.push(` Warned: ${report.warns}`);
|
|
91
|
+
lines.push("");
|
|
92
|
+
if (report.top_blocked_tools.length > 0) {
|
|
93
|
+
lines.push("Top blocked tools:");
|
|
94
|
+
for (const t of report.top_blocked_tools) {
|
|
95
|
+
lines.push(` ${t.tool}: ${t.count}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push("");
|
|
98
|
+
}
|
|
99
|
+
if (report.top_blocked_paths.length > 0) {
|
|
100
|
+
lines.push("Top blocked paths:");
|
|
101
|
+
for (const p of report.top_blocked_paths) {
|
|
102
|
+
lines.push(` ${p.path}: ${p.count}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
}
|
|
106
|
+
if (report.top_block_reasons.length > 0) {
|
|
107
|
+
lines.push("Top block reasons:");
|
|
108
|
+
for (const r of report.top_block_reasons) {
|
|
109
|
+
lines.push(` "${r.reason}": ${r.count}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
if (report.suggestions.length > 0) {
|
|
114
|
+
lines.push("Suggestions:");
|
|
115
|
+
for (const s of report.suggestions) {
|
|
116
|
+
lines.push(` - ${s}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
export async function runFeedback(opts) {
|
|
123
|
+
const projectDir = process.cwd();
|
|
124
|
+
const traces = await readTraces(projectDir, opts.days);
|
|
125
|
+
if (traces.length === 0) {
|
|
126
|
+
process.stderr.write(`No trace data found for the last ${opts.days} day(s).\n`);
|
|
127
|
+
process.stderr.write("Traces are generated by dna-hook during Claude Code sessions.\n");
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const stats = aggregate(traces);
|
|
131
|
+
const suggestions = generateSuggestions(stats);
|
|
132
|
+
const report = { ...stats, period_days: opts.days, suggestions };
|
|
133
|
+
if (opts.json) {
|
|
134
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
process.stderr.write(formatReport(report));
|
|
138
|
+
}
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
@@ -38,6 +38,9 @@ const TEMPLATE_KEYWORDS = {
|
|
|
38
38
|
"brainstorming-first": ["brainstorm", "explore", "ideation", "探索"],
|
|
39
39
|
"branch-finishing": ["branch", "finish", "cleanup", "pr prep"],
|
|
40
40
|
"pr-submitter": ["pr", "pull request", "submit"],
|
|
41
|
+
"migration-safety": ["migration", "migrate", "upgrade", "database migration", "dependency upgrade", "迁移"],
|
|
42
|
+
"incident-response": ["incident", "production issue", "hotfix", "outage", "postmortem", "事故"],
|
|
43
|
+
"performance-audit": ["performance", "profiling", "benchmark", "optimize", "latency", "性能"],
|
|
41
44
|
};
|
|
42
45
|
function scoreTemplates(description) {
|
|
43
46
|
const lower = description.toLowerCase();
|
|
@@ -95,7 +95,132 @@ function isPluginRegistered() {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
// ── Main ──────────────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Get the currently installed version of intentdna.
|
|
100
|
+
*/
|
|
101
|
+
function getInstalledVersion() {
|
|
102
|
+
try {
|
|
103
|
+
const result = execSync("npm list -g intentdna --json 2>/dev/null", {
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
timeout: 10000,
|
|
106
|
+
});
|
|
107
|
+
const data = JSON.parse(result);
|
|
108
|
+
return data?.dependencies?.intentdna?.version ?? null;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the latest published version from npm registry.
|
|
116
|
+
*/
|
|
117
|
+
function getLatestVersion() {
|
|
118
|
+
try {
|
|
119
|
+
return execSync("npm view intentdna version 2>/dev/null", {
|
|
120
|
+
encoding: "utf-8",
|
|
121
|
+
timeout: 10000,
|
|
122
|
+
}).trim();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Upgrade flow: npm update → re-register marketplace → re-install plugin.
|
|
130
|
+
*/
|
|
131
|
+
async function runUpgrade(opts) {
|
|
132
|
+
// Step 1: Detect claude CLI
|
|
133
|
+
const claudeVersion = detectClaudeCLI();
|
|
134
|
+
if (!claudeVersion) {
|
|
135
|
+
log("Claude Code CLI not detected. Cannot upgrade plugin.");
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
log(`Claude Code detected: ${claudeVersion}`);
|
|
139
|
+
// Step 2: Check versions
|
|
140
|
+
const currentVer = getInstalledVersion();
|
|
141
|
+
const latestVer = getLatestVersion();
|
|
142
|
+
if (currentVer)
|
|
143
|
+
log(`Current version: ${currentVer}`);
|
|
144
|
+
if (latestVer)
|
|
145
|
+
log(`Latest version: ${latestVer}`);
|
|
146
|
+
if (currentVer && latestVer && currentVer === latestVer) {
|
|
147
|
+
log("Already at latest version.");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Step 3: npm update
|
|
151
|
+
log("");
|
|
152
|
+
log("Updating intentdna...");
|
|
153
|
+
try {
|
|
154
|
+
execSync("npm update -g intentdna", {
|
|
155
|
+
encoding: "utf-8",
|
|
156
|
+
timeout: 60000,
|
|
157
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
158
|
+
});
|
|
159
|
+
const newVer = getInstalledVersion();
|
|
160
|
+
log(`Updated to: ${newVer ?? "unknown"}`);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
164
|
+
log(`npm update failed: ${msg}`);
|
|
165
|
+
log("Try manually: npm update -g intentdna");
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Step 4: Re-resolve package path
|
|
170
|
+
const pkgPath = await resolvePackagePath();
|
|
171
|
+
if (!pkgPath) {
|
|
172
|
+
log("Error: Could not find intentdna package after update.");
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
log(`Package path: ${pkgPath}`);
|
|
176
|
+
// Step 5: Re-register marketplace (picks up new plugin.json version)
|
|
177
|
+
try {
|
|
178
|
+
log("");
|
|
179
|
+
log(`Re-registering marketplace: ${pkgPath}`);
|
|
180
|
+
execSync(`claude plugin marketplace add "${pkgPath}"`, {
|
|
181
|
+
encoding: "utf-8",
|
|
182
|
+
timeout: 30000,
|
|
183
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
184
|
+
});
|
|
185
|
+
log("Marketplace updated.");
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
189
|
+
log(`Warning: marketplace re-register failed: ${msg}`);
|
|
190
|
+
}
|
|
191
|
+
// Step 6: Re-install plugin (picks up new hooks.json)
|
|
192
|
+
try {
|
|
193
|
+
const installCmd = `claude plugin install intentdna@intentdna --scope ${opts.scope}`;
|
|
194
|
+
log(`Re-installing plugin: ${installCmd}`);
|
|
195
|
+
execSync(installCmd, {
|
|
196
|
+
encoding: "utf-8",
|
|
197
|
+
timeout: 30000,
|
|
198
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
199
|
+
});
|
|
200
|
+
log("Plugin re-installed.");
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
204
|
+
log(`Warning: plugin re-install failed: ${msg}`);
|
|
205
|
+
}
|
|
206
|
+
// Step 7: Verify
|
|
207
|
+
if (isPluginRegistered()) {
|
|
208
|
+
log("");
|
|
209
|
+
log("Upgrade complete. Run 'dna sync' to recompile your DNA templates.");
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
log("");
|
|
214
|
+
log("Upgrade completed but plugin verification failed.");
|
|
215
|
+
log("Try: claude plugin list");
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
98
219
|
export async function runSetup(opts) {
|
|
220
|
+
// Upgrade mode
|
|
221
|
+
if (opts.upgrade) {
|
|
222
|
+
return runUpgrade(opts);
|
|
223
|
+
}
|
|
99
224
|
const scope = opts.scope;
|
|
100
225
|
// Step 1: Detect claude CLI
|
|
101
226
|
const claudeVersion = detectClaudeCLI();
|
package/dist/cli/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const HELP = `Intent DNA CLI v0.3.0
|
|
|
17
17
|
Usage: dna <command> [options]
|
|
18
18
|
|
|
19
19
|
Commands:
|
|
20
|
-
setup Register intentdna as a Claude Code plugin (--scope user|project, --yes)
|
|
20
|
+
setup Register intentdna as a Claude Code plugin (--scope user|project, --upgrade, --yes)
|
|
21
21
|
guard Zero-config guardrails — detect environment, apply safety rules
|
|
22
22
|
sync Compile + inject DNA into target file (CLAUDE.md, SOUL.md, etc.)
|
|
23
23
|
verify Verify synced files match .dna/lock checksums (drift detection)
|
|
@@ -29,6 +29,7 @@ Commands:
|
|
|
29
29
|
show Show gene expression state
|
|
30
30
|
evolve Generate epigenetic markers from outcomes
|
|
31
31
|
epigenetic Record outcomes, view markers and summaries
|
|
32
|
+
feedback Analyze trace data, suggest template optimizations (--days <N>, --json)
|
|
32
33
|
|
|
33
34
|
Options:
|
|
34
35
|
--help, -h Show help for a command
|
|
@@ -387,6 +388,7 @@ async function main() {
|
|
|
387
388
|
options: {
|
|
388
389
|
scope: { type: "string", short: "s", default: "user" },
|
|
389
390
|
yes: { type: "boolean", short: "y", default: false },
|
|
391
|
+
upgrade: { type: "boolean", short: "u", default: false },
|
|
390
392
|
},
|
|
391
393
|
strict: false,
|
|
392
394
|
});
|
|
@@ -399,6 +401,24 @@ async function main() {
|
|
|
399
401
|
const code = await runSetup({
|
|
400
402
|
scope: setupScope,
|
|
401
403
|
yes: setupValues.yes,
|
|
404
|
+
upgrade: setupValues.upgrade,
|
|
405
|
+
});
|
|
406
|
+
process.exit(code);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "feedback": {
|
|
410
|
+
const { values: fbValues } = parseArgs({
|
|
411
|
+
args: rest,
|
|
412
|
+
options: {
|
|
413
|
+
days: { type: "string", short: "d", default: "7" },
|
|
414
|
+
json: { type: "boolean", default: false },
|
|
415
|
+
},
|
|
416
|
+
strict: false,
|
|
417
|
+
});
|
|
418
|
+
const { runFeedback } = await import("./commands/feedback.js");
|
|
419
|
+
const code = await runFeedback({
|
|
420
|
+
days: parseInt(fbValues.days, 10) || 7,
|
|
421
|
+
json: fbValues.json,
|
|
402
422
|
});
|
|
403
423
|
process.exit(code);
|
|
404
424
|
break;
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -14,4 +14,23 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Fail-open: all errors → { continue: true, suppressOutput: true }
|
|
16
16
|
*/
|
|
17
|
-
export {
|
|
17
|
+
export interface SessionSummary {
|
|
18
|
+
total: number;
|
|
19
|
+
blocks: number;
|
|
20
|
+
warns: number;
|
|
21
|
+
topBlockedTools: Array<{
|
|
22
|
+
tool: string;
|
|
23
|
+
count: number;
|
|
24
|
+
}>;
|
|
25
|
+
topBlockedPaths: Array<{
|
|
26
|
+
path: string;
|
|
27
|
+
count: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export declare function computeSummary(traces: Array<{
|
|
31
|
+
decision: string;
|
|
32
|
+
tool_name?: string;
|
|
33
|
+
target_path?: string;
|
|
34
|
+
reason?: string;
|
|
35
|
+
}>): SessionSummary;
|
|
36
|
+
export declare function formatSummary(s: SessionSummary): string | null;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -17,15 +17,54 @@
|
|
|
17
17
|
import { readFile, stat } from "node:fs/promises";
|
|
18
18
|
import { resolve } from "node:path";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
|
-
import { readStdin, writeOutput, silentOutput } from "./protocol.js";
|
|
20
|
+
import { readStdin, writeOutput, silentOutput, allowOutput } from "./protocol.js";
|
|
21
21
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, } from "./enforce.js";
|
|
22
|
-
import { appendAudit, readWorkflowState, appendTrace, rotateTraces } from "./state.js";
|
|
22
|
+
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces } from "./state.js";
|
|
23
23
|
// ── Constants ──────────────────────────────────────────────
|
|
24
24
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
25
25
|
const VALID_EVENTS = new Set([
|
|
26
26
|
"PreToolUse", "PostToolUse", "UserPromptSubmit",
|
|
27
27
|
"SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart",
|
|
28
28
|
]);
|
|
29
|
+
export function computeSummary(traces) {
|
|
30
|
+
let blocks = 0, warns = 0;
|
|
31
|
+
const toolCounts = new Map();
|
|
32
|
+
const pathCounts = new Map();
|
|
33
|
+
for (const t of traces) {
|
|
34
|
+
if (t.decision === "block") {
|
|
35
|
+
blocks++;
|
|
36
|
+
if (t.tool_name)
|
|
37
|
+
toolCounts.set(t.tool_name, (toolCounts.get(t.tool_name) ?? 0) + 1);
|
|
38
|
+
if (t.target_path)
|
|
39
|
+
pathCounts.set(t.target_path, (pathCounts.get(t.target_path) ?? 0) + 1);
|
|
40
|
+
}
|
|
41
|
+
else if (t.decision === "warn") {
|
|
42
|
+
warns++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const topBlockedTools = [...toolCounts.entries()]
|
|
46
|
+
.sort((a, b) => b[1] - a[1])
|
|
47
|
+
.slice(0, 3)
|
|
48
|
+
.map(([tool, count]) => ({ tool, count }));
|
|
49
|
+
const topBlockedPaths = [...pathCounts.entries()]
|
|
50
|
+
.sort((a, b) => b[1] - a[1])
|
|
51
|
+
.slice(0, 3)
|
|
52
|
+
.map(([path, count]) => ({ path, count }));
|
|
53
|
+
return { total: traces.length, blocks, warns, topBlockedTools, topBlockedPaths };
|
|
54
|
+
}
|
|
55
|
+
export function formatSummary(s) {
|
|
56
|
+
if (s.blocks === 0 && s.warns === 0)
|
|
57
|
+
return null;
|
|
58
|
+
const lines = [];
|
|
59
|
+
lines.push(`[Intent DNA] Session summary: ${s.total} events, ${s.blocks} blocked, ${s.warns} warned`);
|
|
60
|
+
if (s.topBlockedTools.length > 0) {
|
|
61
|
+
lines.push(" Top blocked tools: " + s.topBlockedTools.map(t => `${t.tool}(${t.count})`).join(", "));
|
|
62
|
+
}
|
|
63
|
+
if (s.topBlockedPaths.length > 0) {
|
|
64
|
+
lines.push(" Top blocked paths: " + s.topBlockedPaths.map(p => `${p.path}(${p.count})`).join(", "));
|
|
65
|
+
}
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
29
68
|
// ── Main ───────────────────────────────────────────────────
|
|
30
69
|
async function main() {
|
|
31
70
|
// Parse args
|
|
@@ -67,7 +106,7 @@ async function main() {
|
|
|
67
106
|
state.existingArtifactPaths = await scanConsumedArtifactPaths(projectDir, ir, wfState.workflow, wfState.current_step);
|
|
68
107
|
}
|
|
69
108
|
}
|
|
70
|
-
// Special handling for Stop — needs async workflow state read
|
|
109
|
+
// Special handling for Stop — needs async workflow state read + session summary
|
|
71
110
|
if (event === "Stop") {
|
|
72
111
|
const wfState = await readWorkflowState(projectDir, sessionId);
|
|
73
112
|
const stopContext = wfState ? {
|
|
@@ -78,11 +117,28 @@ async function main() {
|
|
|
78
117
|
started_at: wfState.started_at,
|
|
79
118
|
completed_artifacts: wfState.completed_artifacts,
|
|
80
119
|
} : null;
|
|
81
|
-
|
|
120
|
+
let stopOutput = enforceStop(ir, {
|
|
82
121
|
cwd: typeof rawInput.cwd === "string" ? rawInput.cwd : undefined,
|
|
83
122
|
session_id: sessionId,
|
|
84
123
|
stop_reason: typeof rawInput.stop_reason === "string" ? rawInput.stop_reason : undefined,
|
|
85
124
|
}, stopContext);
|
|
125
|
+
// Session summary: aggregate block/warn stats from trace
|
|
126
|
+
try {
|
|
127
|
+
const traces = await readTraces(projectDir, 1);
|
|
128
|
+
const summary = computeSummary(traces);
|
|
129
|
+
const summaryText = formatSummary(summary);
|
|
130
|
+
if (summaryText) {
|
|
131
|
+
if (stopOutput.suppressOutput) {
|
|
132
|
+
// Was silent → upgrade to allow with summary
|
|
133
|
+
stopOutput = allowOutput(summaryText, "Stop");
|
|
134
|
+
}
|
|
135
|
+
else if (stopOutput.reason) {
|
|
136
|
+
// Was a block → append summary to reason
|
|
137
|
+
stopOutput.reason += "\n\n" + summaryText;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch { /* fail-open: summary failure never blocks */ }
|
|
86
142
|
writeOutput(stopOutput);
|
|
87
143
|
// Trace for Stop
|
|
88
144
|
appendTrace(projectDir, {
|
|
@@ -91,7 +147,7 @@ async function main() {
|
|
|
91
147
|
workflow: wfState?.workflow,
|
|
92
148
|
step: wfState?.current_step,
|
|
93
149
|
decision: stopOutput.continue === false ? "block" : "allow",
|
|
94
|
-
duration_ms:
|
|
150
|
+
duration_ms: 0,
|
|
95
151
|
timestamp: new Date().toISOString(),
|
|
96
152
|
}).catch(() => { });
|
|
97
153
|
return;
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Step checkpoints are handled by the CLI, not here.
|
|
15
15
|
*/
|
|
16
16
|
import { allowOutput, blockOutput, escalateOutput, silentOutput } from "./protocol.js";
|
|
17
|
+
import { relative } from "node:path";
|
|
17
18
|
// ── PreToolUse Enforcement ─────────────────────────────────
|
|
18
19
|
/**
|
|
19
20
|
* Enforce PreToolUse constraints.
|
|
@@ -319,6 +320,9 @@ function enforceRoleScope(rolesScopeMap, input) {
|
|
|
319
320
|
const filePath = extractFilePath(input);
|
|
320
321
|
if (!filePath)
|
|
321
322
|
return null;
|
|
323
|
+
// Convert absolute path to relative for glob matching
|
|
324
|
+
const cwd = input.cwd;
|
|
325
|
+
const relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
|
|
322
326
|
for (const entry of rolesScopeMap) {
|
|
323
327
|
const agentTypeName = `dna-${toKebabCase(entry.role_name)}`;
|
|
324
328
|
if (input.agent_type !== agentTypeName)
|
|
@@ -327,7 +331,7 @@ function enforceRoleScope(rolesScopeMap, input) {
|
|
|
327
331
|
if (writeGlobs.length === 0) {
|
|
328
332
|
return blockOutput(`[Intent DNA]: Role '${entry.role_name}' has no write permission (path: ${filePath})`);
|
|
329
333
|
}
|
|
330
|
-
if (!checkWriteAllowed(
|
|
334
|
+
if (!checkWriteAllowed(relativePath, writeGlobs)) {
|
|
331
335
|
return blockOutput(`[Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${writeGlobs.join(", ")})`);
|
|
332
336
|
}
|
|
333
337
|
return null; // Role matched, write allowed
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@
|
|
|
7
7
|
export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, type SessionStartInput as SessionStartHookInput, readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
8
8
|
export { type EnforceState, type SessionStartInput, type StopEnforceInput, type StopWorkflowContext, enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
9
9
|
export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
|
|
10
|
+
export { type SessionSummary, computeSummary, formatSummary, } from "./cli.js";
|
package/dist/hooks/index.js
CHANGED
|
@@ -10,3 +10,5 @@ export { readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silen
|
|
|
10
10
|
export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
11
11
|
// State management
|
|
12
12
|
export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
|
|
13
|
+
// Session summary
|
|
14
|
+
export { computeSummary, formatSummary, } from "./cli.js";
|
|
@@ -63,6 +63,14 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
63
63
|
}
|
|
64
64
|
lines.push("</Steps>");
|
|
65
65
|
lines.push("");
|
|
66
|
+
// Execution Policy — force-complete standard
|
|
67
|
+
lines.push("<Execution_Policy>");
|
|
68
|
+
lines.push("- Use TodoWrite to track each step as pending/in_progress/completed.");
|
|
69
|
+
lines.push("- After completing each step, immediately proceed to the next — do not stop, summarize, or wait for confirmation.");
|
|
70
|
+
lines.push("- If a step fails, mark it as failed in TodoWrite, log the error, and continue to the next non-dependent step.");
|
|
71
|
+
lines.push("- Do not ask the user for permission between steps — the workflow is pre-approved.");
|
|
72
|
+
lines.push("</Execution_Policy>");
|
|
73
|
+
lines.push("");
|
|
66
74
|
// Tool_Usage — only if roles have permissions or scope
|
|
67
75
|
const toolUsageLines = [];
|
|
68
76
|
for (const roleName of usedRoles) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# incident-response: 生产事故响应
|
|
2
|
+
# 适用: 线上故障排查、紧急修复、事后复盘
|
|
3
|
+
# 核心: 快速止血、根因分析、防止复发
|
|
4
|
+
|
|
5
|
+
version: "0.1.0"
|
|
6
|
+
id: template_incident_response
|
|
7
|
+
name: Incident Response
|
|
8
|
+
type: project
|
|
9
|
+
namespace: ir
|
|
10
|
+
|
|
11
|
+
cascade:
|
|
12
|
+
inherits: ["species:default"]
|
|
13
|
+
priority: 100
|
|
14
|
+
|
|
15
|
+
genes:
|
|
16
|
+
triage_first:
|
|
17
|
+
description: Assess severity and blast radius before fixing
|
|
18
|
+
codons:
|
|
19
|
+
- type: attract
|
|
20
|
+
target: severity_assessment
|
|
21
|
+
- type: attract
|
|
22
|
+
target: blast_radius_check
|
|
23
|
+
- type: repel
|
|
24
|
+
target: premature_fix
|
|
25
|
+
- type: sense
|
|
26
|
+
signal: production_alert
|
|
27
|
+
response: prioritize_investigation
|
|
28
|
+
|
|
29
|
+
minimal_fix:
|
|
30
|
+
description: Apply the smallest change that stops the bleeding
|
|
31
|
+
codons:
|
|
32
|
+
- type: attract
|
|
33
|
+
target: targeted_hotfix
|
|
34
|
+
- type: attract
|
|
35
|
+
target: revert_option
|
|
36
|
+
- type: repel
|
|
37
|
+
target: refactor_during_incident
|
|
38
|
+
- type: weight
|
|
39
|
+
a: speed
|
|
40
|
+
b: elegance
|
|
41
|
+
ratio: 0.9
|
|
42
|
+
|
|
43
|
+
evidence_driven:
|
|
44
|
+
description: Collect evidence before hypothesizing
|
|
45
|
+
codons:
|
|
46
|
+
- type: attract
|
|
47
|
+
target: logs_and_metrics
|
|
48
|
+
- type: attract
|
|
49
|
+
target: reproduction_steps
|
|
50
|
+
- type: repel
|
|
51
|
+
target: guessing_root_cause
|
|
52
|
+
- type: threshold
|
|
53
|
+
condition: "has_evidence == true"
|
|
54
|
+
action: escalate
|
|
55
|
+
|
|
56
|
+
prevent_recurrence:
|
|
57
|
+
description: Every fix must include prevention measures
|
|
58
|
+
codons:
|
|
59
|
+
- type: attract
|
|
60
|
+
target: regression_test
|
|
61
|
+
- type: attract
|
|
62
|
+
target: monitoring_alert
|
|
63
|
+
- type: repel
|
|
64
|
+
target: fix_without_test
|
|
65
|
+
|
|
66
|
+
contexts: {}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# migration-safety: 安全迁移
|
|
2
|
+
# 适用: 数据库迁移、依赖升级、框架版本迁移、API 版本切换
|
|
3
|
+
# 核心: 回滚优先、渐进式、验证每一步
|
|
4
|
+
|
|
5
|
+
version: "0.1.0"
|
|
6
|
+
id: template_migration_safety
|
|
7
|
+
name: Migration Safety
|
|
8
|
+
type: project
|
|
9
|
+
namespace: mig
|
|
10
|
+
|
|
11
|
+
cascade:
|
|
12
|
+
inherits: ["species:default"]
|
|
13
|
+
priority: 100
|
|
14
|
+
|
|
15
|
+
genes:
|
|
16
|
+
rollback_first:
|
|
17
|
+
description: Every migration step must have a rollback plan
|
|
18
|
+
codons:
|
|
19
|
+
- type: attract
|
|
20
|
+
target: reversible_changes
|
|
21
|
+
- type: attract
|
|
22
|
+
target: rollback_script
|
|
23
|
+
- type: repel
|
|
24
|
+
target: destructive_migration
|
|
25
|
+
- type: threshold
|
|
26
|
+
condition: "has_rollback_plan == true"
|
|
27
|
+
action: block
|
|
28
|
+
|
|
29
|
+
incremental_migration:
|
|
30
|
+
description: Migrate incrementally, verify at each stage
|
|
31
|
+
codons:
|
|
32
|
+
- type: attract
|
|
33
|
+
target: phased_migration
|
|
34
|
+
- type: attract
|
|
35
|
+
target: canary_first
|
|
36
|
+
- type: repel
|
|
37
|
+
target: big_bang_migration
|
|
38
|
+
- type: weight
|
|
39
|
+
a: safety
|
|
40
|
+
b: speed
|
|
41
|
+
ratio: 0.9
|
|
42
|
+
|
|
43
|
+
backward_compatible:
|
|
44
|
+
description: Maintain backward compatibility during transition
|
|
45
|
+
codons:
|
|
46
|
+
- type: attract
|
|
47
|
+
target: dual_write
|
|
48
|
+
- type: attract
|
|
49
|
+
target: feature_flags
|
|
50
|
+
- type: repel
|
|
51
|
+
target: breaking_change_without_migration
|
|
52
|
+
- type: sense
|
|
53
|
+
signal: deprecation_warning
|
|
54
|
+
response: plan_migration_path
|
|
55
|
+
|
|
56
|
+
verify_data_integrity:
|
|
57
|
+
description: Validate data consistency after each migration step
|
|
58
|
+
codons:
|
|
59
|
+
- type: attract
|
|
60
|
+
target: data_validation_checks
|
|
61
|
+
- type: attract
|
|
62
|
+
target: diff_before_after
|
|
63
|
+
- type: threshold
|
|
64
|
+
condition: "data_loss == false"
|
|
65
|
+
action: block
|
|
66
|
+
|
|
67
|
+
contexts: {}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# performance-audit: 性能优化
|
|
2
|
+
# 适用: 性能分析、基准测试、瓶颈排查、优化实施
|
|
3
|
+
# 核心: 先测量再优化、数据驱动、避免过早优化
|
|
4
|
+
|
|
5
|
+
version: "0.1.0"
|
|
6
|
+
id: template_performance_audit
|
|
7
|
+
name: Performance Audit
|
|
8
|
+
type: project
|
|
9
|
+
namespace: perf
|
|
10
|
+
|
|
11
|
+
cascade:
|
|
12
|
+
inherits: ["species:default"]
|
|
13
|
+
priority: 100
|
|
14
|
+
|
|
15
|
+
genes:
|
|
16
|
+
measure_first:
|
|
17
|
+
description: Profile and benchmark before making any change
|
|
18
|
+
codons:
|
|
19
|
+
- type: attract
|
|
20
|
+
target: profiling_data
|
|
21
|
+
- type: attract
|
|
22
|
+
target: baseline_benchmark
|
|
23
|
+
- type: repel
|
|
24
|
+
target: optimize_without_measurement
|
|
25
|
+
- type: threshold
|
|
26
|
+
condition: "has_baseline_metrics == true"
|
|
27
|
+
action: escalate
|
|
28
|
+
|
|
29
|
+
targeted_optimization:
|
|
30
|
+
description: Optimize the actual bottleneck, not hunches
|
|
31
|
+
codons:
|
|
32
|
+
- type: attract
|
|
33
|
+
target: hotspot_analysis
|
|
34
|
+
- type: attract
|
|
35
|
+
target: algorithmic_improvement
|
|
36
|
+
- type: repel
|
|
37
|
+
target: premature_optimization
|
|
38
|
+
- type: weight
|
|
39
|
+
a: correctness
|
|
40
|
+
b: performance
|
|
41
|
+
ratio: 0.7
|
|
42
|
+
|
|
43
|
+
regression_prevention:
|
|
44
|
+
description: Ensure optimizations don't break functionality
|
|
45
|
+
codons:
|
|
46
|
+
- type: attract
|
|
47
|
+
target: before_after_comparison
|
|
48
|
+
- type: attract
|
|
49
|
+
target: existing_tests_pass
|
|
50
|
+
- type: repel
|
|
51
|
+
target: optimization_breaks_tests
|
|
52
|
+
- type: threshold
|
|
53
|
+
condition: "test_regression == false"
|
|
54
|
+
action: block
|
|
55
|
+
|
|
56
|
+
document_results:
|
|
57
|
+
description: Record measurements and decisions for future reference
|
|
58
|
+
codons:
|
|
59
|
+
- type: attract
|
|
60
|
+
target: performance_report
|
|
61
|
+
- type: attract
|
|
62
|
+
target: optimization_rationale
|
|
63
|
+
- type: sense
|
|
64
|
+
signal: performance_improvement
|
|
65
|
+
response: record_technique
|
|
66
|
+
|
|
67
|
+
contexts: {}
|