@vyuhlabs/dxkit 2.12.0 → 2.13.1
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/CHANGELOG.md +90 -0
- package/README.md +246 -287
- package/dist/allowlist/hint.d.ts +1 -1
- package/dist/allowlist/hint.d.ts.map +1 -1
- package/dist/allowlist/hint.js +6 -3
- package/dist/allowlist/hint.js.map +1 -1
- package/dist/baseline/check.d.ts +7 -0
- package/dist/baseline/check.d.ts.map +1 -1
- package/dist/baseline/check.js +3 -1
- package/dist/baseline/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +101 -14
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/graph-tab.d.ts.map +1 -1
- package/dist/dashboard/graph-tab.js +6 -3
- package/dist/dashboard/graph-tab.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +13 -12
- package/dist/doctor.js.map +1 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +8 -2
- package/dist/generator.js.map +1 -1
- package/dist/issue-cli.d.ts +1 -1
- package/dist/issue-cli.js +1 -1
- package/dist/loop/demo.d.ts +12 -0
- package/dist/loop/demo.d.ts.map +1 -0
- package/dist/loop/demo.js +331 -0
- package/dist/loop/demo.js.map +1 -0
- package/dist/loop/doctor.d.ts +37 -0
- package/dist/loop/doctor.d.ts.map +1 -0
- package/dist/loop/doctor.js +320 -0
- package/dist/loop/doctor.js.map +1 -0
- package/dist/loop/ledger-cli.d.ts +7 -0
- package/dist/loop/ledger-cli.d.ts.map +1 -0
- package/dist/loop/ledger-cli.js +95 -0
- package/dist/loop/ledger-cli.js.map +1 -0
- package/dist/loop/ledger.d.ts +95 -0
- package/dist/loop/ledger.d.ts.map +1 -0
- package/dist/loop/ledger.js +201 -0
- package/dist/loop/ledger.js.map +1 -0
- package/dist/loop/policy.d.ts +35 -0
- package/dist/loop/policy.d.ts.map +1 -0
- package/dist/loop/policy.js +151 -0
- package/dist/loop/policy.js.map +1 -0
- package/dist/loop/scaffold.d.ts +28 -0
- package/dist/loop/scaffold.d.ts.map +1 -0
- package/dist/loop/scaffold.js +224 -0
- package/dist/loop/scaffold.js.map +1 -0
- package/dist/loop/stop-gate.d.ts +71 -0
- package/dist/loop/stop-gate.d.ts.map +1 -0
- package/dist/loop/stop-gate.js +295 -0
- package/dist/loop/stop-gate.js.map +1 -0
- package/dist/self-invocation.d.ts +77 -0
- package/dist/self-invocation.d.ts.map +1 -0
- package/dist/self-invocation.js +157 -0
- package/dist/self-invocation.js.map +1 -0
- package/dist/ship-installers.d.ts.map +1 -1
- package/dist/ship-installers.js +8 -0
- package/dist/ship-installers.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +22 -5
- package/dist/update.js.map +1 -1
- package/dist/upgrade.d.ts +3 -3
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +5 -4
- package/dist/upgrade.js.map +1 -1
- package/package.json +6 -4
- package/templates/.claude/skills/dxkit-config/SKILL.md +17 -0
- package/templates/.claude/skills/dxkit-init/SKILL.md +1 -0
- package/templates/.claude/skills/dxkit-learn/SKILL.md +17 -0
- package/templates/.claude/skills/dxkit-loop/SKILL.md +114 -0
- package/templates/.claude/skills/dxkit-onboard/SKILL.md +2 -0
- package/templates/.claude/skills/dxkit-update/SKILL.md +3 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LEDGER_FILE = exports.LEDGER_DIR = exports.LEDGER_SCHEMA_VERSION = void 0;
|
|
37
|
+
exports.buildLedgerEvent = buildLedgerEvent;
|
|
38
|
+
exports.appendLedgerEvent = appendLedgerEvent;
|
|
39
|
+
exports.readLedger = readLedger;
|
|
40
|
+
exports.clearLedger = clearLedger;
|
|
41
|
+
exports.summarizeLedger = summarizeLedger;
|
|
42
|
+
/**
|
|
43
|
+
* Loop ledger — an append-only audit trail of postflight (Stop-gate)
|
|
44
|
+
* events for autonomous coding loops.
|
|
45
|
+
*
|
|
46
|
+
* Every time the Stop-gate runs (whether it allows the loop to stop or
|
|
47
|
+
* blocks it on net-new findings), it appends one line to
|
|
48
|
+
* `.dxkit/loop/ledger.jsonl`. The ledger answers "what did the loop
|
|
49
|
+
* actually do?" — how many completions were blocked, how many net-new
|
|
50
|
+
* findings, and whether the agent repaired after a block.
|
|
51
|
+
*
|
|
52
|
+
* This is deliberately NOT a dashboard. It is a flat JSONL file so a
|
|
53
|
+
* loop can write to it from any process without coordination, and a
|
|
54
|
+
* human (or a benchmark harness) can `cat` / `jq` it directly.
|
|
55
|
+
*/
|
|
56
|
+
const child_process_1 = require("child_process");
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
/** Bump only on a breaking change to the event shape. */
|
|
60
|
+
exports.LEDGER_SCHEMA_VERSION = 1;
|
|
61
|
+
/** Relative location of the ledger inside a repo. */
|
|
62
|
+
exports.LEDGER_DIR = path.join('.dxkit', 'loop');
|
|
63
|
+
exports.LEDGER_FILE = path.join(exports.LEDGER_DIR, 'ledger.jsonl');
|
|
64
|
+
/** Best-effort current branch; empty string when not derivable. */
|
|
65
|
+
function gitBranch(cwd) {
|
|
66
|
+
try {
|
|
67
|
+
return (0, child_process_1.execFileSync)('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
68
|
+
cwd,
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
71
|
+
}).trim();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Best-effort current commit SHA; empty string when not derivable. */
|
|
78
|
+
function gitCommit(cwd) {
|
|
79
|
+
try {
|
|
80
|
+
return (0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
84
|
+
}).trim();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fill in the repo-derived fields (branch, commit) and stamp the
|
|
92
|
+
* schema version + timestamp, so callers only supply the outcome.
|
|
93
|
+
*/
|
|
94
|
+
function buildLedgerEvent(cwd, fields) {
|
|
95
|
+
return {
|
|
96
|
+
schema_version: exports.LEDGER_SCHEMA_VERSION,
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
event: 'Stop',
|
|
99
|
+
branch: fields.branch ?? gitBranch(cwd),
|
|
100
|
+
commit: fields.commit ?? gitCommit(cwd),
|
|
101
|
+
...fields,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Append one event to the ledger. Creates `.dxkit/loop/` on demand.
|
|
106
|
+
* Best-effort: a ledger write must never abort the Stop-gate (the
|
|
107
|
+
* gate's verdict matters more than its audit line), so failures are
|
|
108
|
+
* swallowed and reported via the return value.
|
|
109
|
+
*/
|
|
110
|
+
function appendLedgerEvent(cwd, event) {
|
|
111
|
+
try {
|
|
112
|
+
const dir = path.join(cwd, exports.LEDGER_DIR);
|
|
113
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
114
|
+
fs.appendFileSync(path.join(cwd, exports.LEDGER_FILE), JSON.stringify(event) + '\n', 'utf8');
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Read every ledger event. Returns [] when the ledger is absent. */
|
|
122
|
+
function readLedger(cwd) {
|
|
123
|
+
const file = path.join(cwd, exports.LEDGER_FILE);
|
|
124
|
+
let raw;
|
|
125
|
+
try {
|
|
126
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const line of raw.split('\n')) {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (!trimmed)
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
out.push(JSON.parse(trimmed));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Skip a corrupt line rather than failing the whole read — the
|
|
141
|
+
// ledger is append-only and a partial write shouldn't blind the
|
|
142
|
+
// summary to every other event.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** Remove the ledger file. Returns true when a file was deleted. */
|
|
148
|
+
function clearLedger(cwd) {
|
|
149
|
+
try {
|
|
150
|
+
fs.rmSync(path.join(cwd, exports.LEDGER_FILE));
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Reduce a list of events to the audit-trail summary. Repair detection
|
|
159
|
+
* is per-session: a session counts as "repaired" if it has at least one
|
|
160
|
+
* blocked event AND a strictly-later allowed event with a passing
|
|
161
|
+
* guardrail. Order is taken from event position (the ledger is
|
|
162
|
+
* append-only, so file order is chronological).
|
|
163
|
+
*/
|
|
164
|
+
function summarizeLedger(events) {
|
|
165
|
+
let allowed = 0;
|
|
166
|
+
let blocked = 0;
|
|
167
|
+
let netNewBlocked = 0;
|
|
168
|
+
// Per-session timeline of (blocked?, repaired?) — repaired means a
|
|
169
|
+
// clean allowed+pass event appeared after the session's first block.
|
|
170
|
+
const blockedAt = new Map(); // session → index of first block
|
|
171
|
+
const repairedSessions = new Set();
|
|
172
|
+
const blockedSessions = new Set();
|
|
173
|
+
events.forEach((e, idx) => {
|
|
174
|
+
if (e.allowed)
|
|
175
|
+
allowed++;
|
|
176
|
+
else {
|
|
177
|
+
blocked++;
|
|
178
|
+
netNewBlocked += e.net_new_findings;
|
|
179
|
+
}
|
|
180
|
+
const sid = e.session_id || '(unknown)';
|
|
181
|
+
if (!e.allowed) {
|
|
182
|
+
blockedSessions.add(sid);
|
|
183
|
+
if (!blockedAt.has(sid))
|
|
184
|
+
blockedAt.set(sid, idx);
|
|
185
|
+
}
|
|
186
|
+
else if (e.guardrail_status === 'pass' &&
|
|
187
|
+
blockedAt.has(sid) &&
|
|
188
|
+
idx > blockedAt.get(sid)) {
|
|
189
|
+
repairedSessions.add(sid);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
total: events.length,
|
|
194
|
+
allowed,
|
|
195
|
+
blocked,
|
|
196
|
+
netNewBlocked,
|
|
197
|
+
repairedAfterBlock: repairedSessions.size,
|
|
198
|
+
unrepairedSessions: [...blockedSessions].filter((s) => !repairedSessions.has(s)).length,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/loop/ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type BrownfieldPolicy } from '../baseline/policy';
|
|
2
|
+
/**
|
|
3
|
+
* The two shipped loop postures.
|
|
4
|
+
* - `security-only` (default): block only on net-new secrets + crit/high
|
|
5
|
+
* security + crit/high reachable dependency vulns. test-gap + quality
|
|
6
|
+
* are NOT blocked — they warn. Cost-bounded; safe to run unattended.
|
|
7
|
+
* - `full-debt`: block on every net-new finding (adds test-gap +
|
|
8
|
+
* quality). Exhaustive but can drive an open-ended repair; opt-in.
|
|
9
|
+
*/
|
|
10
|
+
export type LoopPreset = 'security-only' | 'full-debt';
|
|
11
|
+
/** The cost-bounded default — the posture an unattended loop gets unless
|
|
12
|
+
* the repo explicitly opts into `full-debt`. */
|
|
13
|
+
export declare const DEFAULT_LOOP_PRESET: LoopPreset;
|
|
14
|
+
/** Resolved loop posture: the policy the Stop-gate hands to the guardrail,
|
|
15
|
+
* plus the preset name that produced it (recorded in the ledger). */
|
|
16
|
+
export interface ResolvedLoopPolicy {
|
|
17
|
+
readonly policy: BrownfieldPolicy;
|
|
18
|
+
readonly preset: LoopPreset;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the active loop preset. Precedence (mirrors the other
|
|
22
|
+
* `DXKIT_LOOP_*` knobs the Stop-gate already honours):
|
|
23
|
+
* 1. `DXKIT_LOOP_PRESET` env var (benchmark / CI override).
|
|
24
|
+
* 2. `.dxkit/policy.json` → `loop.preset`.
|
|
25
|
+
* 3. `DEFAULT_LOOP_PRESET` (`security-only`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveLoopPreset(cwd: string): LoopPreset;
|
|
28
|
+
/**
|
|
29
|
+
* Build the loop-scoped policy: the repo's base `BrownfieldPolicy`
|
|
30
|
+
* (confidence thresholds, baseline mode, drift handling preserved) with
|
|
31
|
+
* its `block` list + `blockRules` REPLACED by the active preset's. Only
|
|
32
|
+
* the Stop-gate calls this — see the scope note at the top of the file.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveLoopPolicy(cwd: string): ResolvedLoopPolicy;
|
|
35
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/loop/policy.ts"],"names":[],"mappings":"AA0BA,OAAO,EAEL,KAAK,gBAAgB,EAGtB,MAAM,oBAAoB,CAAC;AAG5B;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,WAAW,CAAC;AAEvD;iDACiD;AACjD,eAAO,MAAM,mBAAmB,EAAE,UAA4B,CAAC;AA8C/D;sEACsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B;AAuBD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAIzD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAYjE"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_LOOP_PRESET = void 0;
|
|
37
|
+
exports.resolveLoopPreset = resolveLoopPreset;
|
|
38
|
+
exports.resolveLoopPolicy = resolveLoopPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* Loop policy presets — a curated blocking posture scoped to autonomous
|
|
41
|
+
* coding loops ONLY.
|
|
42
|
+
*
|
|
43
|
+
* ──────────────────────────────────────────────────────────────────────
|
|
44
|
+
* SCOPE: this layer is read by exactly one consumer — the Stop-gate
|
|
45
|
+
* (`src/loop/stop-gate.ts`). The CI / PR guardrail (`vyuh-dxkit baseline
|
|
46
|
+
* check`) and `createBaseline` resolve the shared `BrownfieldPolicy`
|
|
47
|
+
* directly via `resolvePolicy` and NEVER read `loop.preset`. So setting a
|
|
48
|
+
* preset changes how an unattended loop blocks WITHOUT silently
|
|
49
|
+
* downgrading a repo's CI posture. The `loop.*` namespace in
|
|
50
|
+
* `.dxkit/policy.json` and the `Loop`-prefixed names here both signal
|
|
51
|
+
* that boundary.
|
|
52
|
+
* ──────────────────────────────────────────────────────────────────────
|
|
53
|
+
*
|
|
54
|
+
* Why a loop-only posture exists: in CI a guardrail block just fails a
|
|
55
|
+
* check a human then reads — blocking on every debt class (test-gap,
|
|
56
|
+
* quality) is fine. In a loop a block instead FEEDS THE MODEL a repair
|
|
57
|
+
* instruction, so blocking on open-ended debt makes the agent grind on it
|
|
58
|
+
* unattended (writing tests / refactoring until the gap closes), which is
|
|
59
|
+
* expensive and unbounded. The default loop posture therefore blocks only
|
|
60
|
+
* on the unambiguous, must-fix security class; the open-ended debt classes
|
|
61
|
+
* are an explicit opt-in.
|
|
62
|
+
*/
|
|
63
|
+
const fs = __importStar(require("fs"));
|
|
64
|
+
const path = __importStar(require("path"));
|
|
65
|
+
const policy_1 = require("../baseline/policy");
|
|
66
|
+
/** The cost-bounded default — the posture an unattended loop gets unless
|
|
67
|
+
* the repo explicitly opts into `full-debt`. */
|
|
68
|
+
exports.DEFAULT_LOOP_PRESET = 'security-only';
|
|
69
|
+
/** The security class: secrets, crit/high SAST, crit/high reachable dep
|
|
70
|
+
* vulns. Shared by both presets — full-debt is this plus the debt rules. */
|
|
71
|
+
const SECURITY_BLOCK_RULES = {
|
|
72
|
+
newSecret: true,
|
|
73
|
+
newCriticalSecurity: true,
|
|
74
|
+
newHighSecurity: true,
|
|
75
|
+
newCriticalDependencyVulnerability: true,
|
|
76
|
+
newHighReachableDependencyVulnerability: true,
|
|
77
|
+
// Open-ended debt — OFF in security-only (warn, never block in a loop).
|
|
78
|
+
newUntestedChangedSource: false,
|
|
79
|
+
newSevereQualityIssueInChangedFiles: false,
|
|
80
|
+
};
|
|
81
|
+
const PRESETS = Object.freeze({
|
|
82
|
+
'security-only': {
|
|
83
|
+
// Empty generic block list: nothing auto-blocks by status alone, so
|
|
84
|
+
// test-gap + quality net-new findings warn but never block the loop.
|
|
85
|
+
// Blocking comes solely from SECURITY_BLOCK_RULES.
|
|
86
|
+
block: [],
|
|
87
|
+
blockRules: SECURITY_BLOCK_RULES,
|
|
88
|
+
},
|
|
89
|
+
'full-debt': {
|
|
90
|
+
// Any net-new finding blocks (generic `added`), plus every escalation.
|
|
91
|
+
block: ['added'],
|
|
92
|
+
blockRules: {
|
|
93
|
+
...SECURITY_BLOCK_RULES,
|
|
94
|
+
newUntestedChangedSource: true,
|
|
95
|
+
newSevereQualityIssueInChangedFiles: true,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
function isLoopPreset(v) {
|
|
100
|
+
return v === 'security-only' || v === 'full-debt';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read `loop.preset` from `.dxkit/policy.json`. Best-effort: a missing /
|
|
104
|
+
* malformed file or absent `loop` block yields `undefined` so the caller
|
|
105
|
+
* falls back to the default. Read here (not via `resolvePolicy`) so the
|
|
106
|
+
* loop concept stays out of the shared `BrownfieldPolicy` schema.
|
|
107
|
+
*/
|
|
108
|
+
function readPresetFromPolicyFile(cwd) {
|
|
109
|
+
try {
|
|
110
|
+
const raw = fs.readFileSync(path.join(cwd, policy_1.DEFAULT_POLICY_FILENAME), 'utf8');
|
|
111
|
+
const parsed = JSON.parse(raw);
|
|
112
|
+
const preset = parsed.loop?.preset;
|
|
113
|
+
return isLoopPreset(preset) ? preset : undefined;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the active loop preset. Precedence (mirrors the other
|
|
121
|
+
* `DXKIT_LOOP_*` knobs the Stop-gate already honours):
|
|
122
|
+
* 1. `DXKIT_LOOP_PRESET` env var (benchmark / CI override).
|
|
123
|
+
* 2. `.dxkit/policy.json` → `loop.preset`.
|
|
124
|
+
* 3. `DEFAULT_LOOP_PRESET` (`security-only`).
|
|
125
|
+
*/
|
|
126
|
+
function resolveLoopPreset(cwd) {
|
|
127
|
+
const env = process.env.DXKIT_LOOP_PRESET;
|
|
128
|
+
if (isLoopPreset(env))
|
|
129
|
+
return env;
|
|
130
|
+
return readPresetFromPolicyFile(cwd) ?? exports.DEFAULT_LOOP_PRESET;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build the loop-scoped policy: the repo's base `BrownfieldPolicy`
|
|
134
|
+
* (confidence thresholds, baseline mode, drift handling preserved) with
|
|
135
|
+
* its `block` list + `blockRules` REPLACED by the active preset's. Only
|
|
136
|
+
* the Stop-gate calls this — see the scope note at the top of the file.
|
|
137
|
+
*/
|
|
138
|
+
function resolveLoopPolicy(cwd) {
|
|
139
|
+
const base = (0, policy_1.resolvePolicy)(undefined, cwd);
|
|
140
|
+
const preset = resolveLoopPreset(cwd);
|
|
141
|
+
const def = PRESETS[preset];
|
|
142
|
+
return {
|
|
143
|
+
preset,
|
|
144
|
+
policy: {
|
|
145
|
+
...base,
|
|
146
|
+
block: def.block,
|
|
147
|
+
blockRules: def.blockRules,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/loop/policy.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+HA,8CAIC;AAQD,8CAYC;AAvJD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,uCAAyB;AACzB,2CAA6B;AAC7B,+CAK4B;AAa5B;iDACiD;AACpC,QAAA,mBAAmB,GAAe,eAAe,CAAC;AAc/D;6EAC6E;AAC7E,MAAM,oBAAoB,GAAyB;IACjD,SAAS,EAAE,IAAI;IACf,mBAAmB,EAAE,IAAI;IACzB,eAAe,EAAE,IAAI;IACrB,kCAAkC,EAAE,IAAI;IACxC,uCAAuC,EAAE,IAAI;IAC7C,wEAAwE;IACxE,wBAAwB,EAAE,KAAK;IAC/B,mCAAmC,EAAE,KAAK;CAC3C,CAAC;AAEF,MAAM,OAAO,GAA4C,MAAM,CAAC,MAAM,CAAC;IACrE,eAAe,EAAE;QACf,oEAAoE;QACpE,qEAAqE;QACrE,mDAAmD;QACnD,KAAK,EAAE,EAAE;QACT,UAAU,EAAE,oBAAoB;KACjC;IACD,WAAW,EAAE;QACX,uEAAuE;QACvE,KAAK,EAAE,CAAC,OAAO,CAAC;QAChB,UAAU,EAAE;YACV,GAAG,oBAAoB;YACvB,wBAAwB,EAAE,IAAI;YAC9B,mCAAmC,EAAE,IAAI;SAC1C;KACF;CACF,CAAC,CAAC;AASH,SAAS,YAAY,CAAC,CAAU;IAC9B,OAAO,CAAC,KAAK,eAAe,IAAI,CAAC,KAAK,WAAW,CAAC;AACpD,CAAC;AAED;;;;;GAKG;AACH,SAAS,wBAAwB,CAAC,GAAW;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gCAAuB,CAAC,EAAE,MAAM,CAAC,CAAC;QAC7E,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoC,CAAC;QAClE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC;QACnC,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,GAAW;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAC1C,IAAI,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAClC,OAAO,wBAAwB,CAAC,GAAG,CAAC,IAAI,2BAAmB,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,GAAW;IAC3C,MAAM,IAAI,GAAG,IAAA,sBAAa,EAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO;QACL,MAAM;QACN,MAAM,EAAE;YACN,GAAG,IAAI;YACP,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,UAAU,EAAE,GAAG,CAAC,UAAU;SAC3B;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ShipInstallResult } from '../ship-installers';
|
|
2
|
+
import { type LoopPreset } from './policy';
|
|
3
|
+
/** The command Claude Code runs on Stop. Built from the canonical CLI
|
|
4
|
+
* invocation (`src/self-invocation.ts`) so the installer, doctor, and any
|
|
5
|
+
* future tooling agree on the exact string and the loop Stop hook is a
|
|
6
|
+
* registered self-invocation surface (devDependency + doctor coverage). */
|
|
7
|
+
export declare const STOP_HOOK_COMMAND: string;
|
|
8
|
+
interface LoopScaffoldOpts {
|
|
9
|
+
/**
|
|
10
|
+
* Explicit loop posture to write into `.dxkit/policy.json`. When set
|
|
11
|
+
* (init with `--loop-preset`), it OVERRIDES any existing preset. When
|
|
12
|
+
* omitted (bare init re-run, or update), an existing preset is PRESERVED
|
|
13
|
+
* and `security-only` is seeded only if none exists — so an upgrade
|
|
14
|
+
* never silently resets a user's chosen posture.
|
|
15
|
+
*/
|
|
16
|
+
readonly preset?: LoopPreset;
|
|
17
|
+
}
|
|
18
|
+
/** True when `.claude/settings.json` already registers the Stop-gate. Used
|
|
19
|
+
* by `update`'s install-flag detection so an upgrade refreshes the loop
|
|
20
|
+
* surface only on repos that opted into it. */
|
|
21
|
+
export declare function isClaudeLoopInstalled(cwd: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Scaffold the loop pack into `cwd`. Additive + idempotent: safe to run on
|
|
24
|
+
* a fresh repo or re-run on one that already has settings/CLAUDE.md/policy.
|
|
25
|
+
*/
|
|
26
|
+
export declare function installClaudeLoop(cwd: string, opts?: LoopScaffoldOpts): ShipInstallResult;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/loop/scaffold.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAGhE;;;4EAG4E;AAC5E,eAAO,MAAM,iBAAiB,QAA6B,CAAC;AA0B5D,UAAU,gBAAgB;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAWD;;gDAEgD;AAChD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAY1D;AA2ID;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,iBAAiB,CAM7F"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.STOP_HOOK_COMMAND = void 0;
|
|
37
|
+
exports.isClaudeLoopInstalled = isClaudeLoopInstalled;
|
|
38
|
+
exports.installClaudeLoop = installClaudeLoop;
|
|
39
|
+
/**
|
|
40
|
+
* `init --claude-loop` scaffolding — wires the loop pack into a repo
|
|
41
|
+
* ADDITIVELY. None of these writers clobber a user's file:
|
|
42
|
+
*
|
|
43
|
+
* - `.claude/settings.json`: deep-merge our Stop hook into `hooks.Stop[]`,
|
|
44
|
+
* preserving every existing hook / permission / key. Idempotent.
|
|
45
|
+
* - `CLAUDE.md`: upsert a sentinel-delimited managed block, never
|
|
46
|
+
* touching prose outside the markers. Idempotent.
|
|
47
|
+
* - `.dxkit/policy.json`: set `loop.preset`, preserving all other policy.
|
|
48
|
+
*
|
|
49
|
+
* Each writer reads the existing file, mutates the minimum, and writes
|
|
50
|
+
* back; a malformed existing file is left untouched and surfaced as a
|
|
51
|
+
* note rather than overwritten. This mirrors the additive `.gitignore`
|
|
52
|
+
* installer (`ship-installers.ts:installIgnoreFiles`), extended to JSON +
|
|
53
|
+
* a Markdown managed block.
|
|
54
|
+
*/
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const policy_1 = require("./policy");
|
|
58
|
+
const self_invocation_1 = require("../self-invocation");
|
|
59
|
+
/** The command Claude Code runs on Stop. Built from the canonical CLI
|
|
60
|
+
* invocation (`src/self-invocation.ts`) so the installer, doctor, and any
|
|
61
|
+
* future tooling agree on the exact string and the loop Stop hook is a
|
|
62
|
+
* registered self-invocation surface (devDependency + doctor coverage). */
|
|
63
|
+
exports.STOP_HOOK_COMMAND = (0, self_invocation_1.dxkitCli)('hook stop-gate');
|
|
64
|
+
/** Sentinel markers bounding the dxkit-managed region of CLAUDE.md. Only
|
|
65
|
+
* the text between them is ever rewritten. */
|
|
66
|
+
const CLAUDE_BLOCK_START = '<!-- dxkit:loop:start -->';
|
|
67
|
+
const CLAUDE_BLOCK_END = '<!-- dxkit:loop:end -->';
|
|
68
|
+
/** Preset-agnostic loop norm. Points at `.dxkit/policy.json` as the
|
|
69
|
+
* source of truth for the active posture, so this prose stays correct
|
|
70
|
+
* when the preset is switched without re-running init. */
|
|
71
|
+
const CLAUDE_LOOP_NORM = `## Autonomous loop safety (dxkit)
|
|
72
|
+
|
|
73
|
+
This repo runs coding loops behind the dxkit Stop-gate: when a loop tries
|
|
74
|
+
to stop, \`vyuh-dxkit hook stop-gate\` re-runs the guardrail and blocks
|
|
75
|
+
completion if the branch introduced net-new findings, handing them back
|
|
76
|
+
for repair. Loop norms:
|
|
77
|
+
|
|
78
|
+
- Fix the net-new finding the gate reports. Do NOT refresh the baseline to
|
|
79
|
+
clear a block, and do NOT fix unrelated pre-existing debt — the gate
|
|
80
|
+
only asks for what this branch introduced.
|
|
81
|
+
- The blocking posture is \`loop.preset\` in \`.dxkit/policy.json\`:
|
|
82
|
+
\`security-only\` (default) blocks net-new secrets + crit/high security +
|
|
83
|
+
reachable dependency vulns; \`full-debt\` also blocks test-gap + quality.
|
|
84
|
+
- \`vyuh-dxkit loop doctor\` verifies the loop is wired safely;
|
|
85
|
+
\`vyuh-dxkit loop ledger summarize\` reports what the gate did.`;
|
|
86
|
+
/** True when `.claude/settings.json` already registers the Stop-gate. Used
|
|
87
|
+
* by `update`'s install-flag detection so an upgrade refreshes the loop
|
|
88
|
+
* surface only on repos that opted into it. */
|
|
89
|
+
function isClaudeLoopInstalled(cwd) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8');
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
return (parsed.hooks?.Stop ?? []).some((e) => (e.hooks ?? []).some((h) => typeof h.command === 'string' && /hook\s+stop-gate/.test(h.command)));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Merge the Stop hook into `.claude/settings.json`. Preserves all existing
|
|
101
|
+
* settings; idempotent (no-op when our hook is already registered). A
|
|
102
|
+
* malformed existing file is left intact and reported via `sidecars`.
|
|
103
|
+
*/
|
|
104
|
+
function mergeStopHook(cwd, result) {
|
|
105
|
+
const rel = path.join('.claude', 'settings.json');
|
|
106
|
+
const abs = path.join(cwd, rel);
|
|
107
|
+
let settings = {};
|
|
108
|
+
let existed = false;
|
|
109
|
+
if (fs.existsSync(abs)) {
|
|
110
|
+
existed = true;
|
|
111
|
+
try {
|
|
112
|
+
settings = JSON.parse(fs.readFileSync(abs, 'utf8'));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Don't clobber a file we can't parse — drop a reference sidecar.
|
|
116
|
+
const sidecar = rel + '.dxkit';
|
|
117
|
+
fs.writeFileSync(path.join(cwd, sidecar), JSON.stringify({ hooks: { Stop: [stopEntry()] } }, null, 2) + '\n', 'utf8');
|
|
118
|
+
result.sidecars.push(sidecar);
|
|
119
|
+
result.notes.push(`${rel} is not valid JSON — left untouched. Merge the Stop hook from ${sidecar} by hand.`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
settings.hooks ??= {};
|
|
124
|
+
settings.hooks.Stop ??= [];
|
|
125
|
+
const already = settings.hooks.Stop.some((e) => (e.hooks ?? []).some((h) => typeof h.command === 'string' && /hook\s+stop-gate/.test(h.command)));
|
|
126
|
+
if (already) {
|
|
127
|
+
result.skipped.push(rel);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
settings.hooks.Stop.push(stopEntry());
|
|
131
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
132
|
+
fs.writeFileSync(abs, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
133
|
+
result.installed.push(rel);
|
|
134
|
+
if (existed) {
|
|
135
|
+
result.notes.push(`Merged the Stop-gate hook into your existing ${rel} (other hooks preserved).`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function stopEntry() {
|
|
139
|
+
// Stop hooks take no matcher (unlike PreToolUse).
|
|
140
|
+
return { hooks: [{ type: 'command', command: exports.STOP_HOOK_COMMAND }] };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Upsert the dxkit loop managed block in CLAUDE.md. Replaces the block
|
|
144
|
+
* between the sentinels if present (idempotent), appends it otherwise, and
|
|
145
|
+
* never touches content outside the markers.
|
|
146
|
+
*/
|
|
147
|
+
function upsertClaudeBlock(cwd, result) {
|
|
148
|
+
const rel = 'CLAUDE.md';
|
|
149
|
+
const abs = path.join(cwd, rel);
|
|
150
|
+
const block = `${CLAUDE_BLOCK_START}\n${CLAUDE_LOOP_NORM}\n${CLAUDE_BLOCK_END}`;
|
|
151
|
+
let existing = '';
|
|
152
|
+
let existed = false;
|
|
153
|
+
if (fs.existsSync(abs)) {
|
|
154
|
+
existed = true;
|
|
155
|
+
existing = fs.readFileSync(abs, 'utf8');
|
|
156
|
+
}
|
|
157
|
+
const startIdx = existing.indexOf(CLAUDE_BLOCK_START);
|
|
158
|
+
const endIdx = existing.indexOf(CLAUDE_BLOCK_END);
|
|
159
|
+
let next;
|
|
160
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
161
|
+
const before = existing.slice(0, startIdx);
|
|
162
|
+
const after = existing.slice(endIdx + CLAUDE_BLOCK_END.length);
|
|
163
|
+
next = before + block + after;
|
|
164
|
+
if (next === existing) {
|
|
165
|
+
result.skipped.push(rel);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (existed) {
|
|
170
|
+
next = existing.replace(/\s*$/, '') + '\n\n' + block + '\n';
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
next = `# CLAUDE.md\n\n${block}\n`;
|
|
174
|
+
}
|
|
175
|
+
fs.writeFileSync(abs, next, 'utf8');
|
|
176
|
+
result.installed.push(rel);
|
|
177
|
+
if (existed && startIdx === -1) {
|
|
178
|
+
result.notes.push(`Appended a dxkit loop block to your existing ${rel} (your content preserved).`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Ensure `loop.preset` in `.dxkit/policy.json`, preserving all other
|
|
183
|
+
* policy fields. `explicit` (init `--loop-preset`) overrides; otherwise an
|
|
184
|
+
* existing preset is preserved and `security-only` is seeded only when
|
|
185
|
+
* none exists. Idempotent; a malformed existing policy is reported, not
|
|
186
|
+
* overwritten.
|
|
187
|
+
*/
|
|
188
|
+
function ensureLoopPreset(cwd, explicit, result) {
|
|
189
|
+
const rel = path.join('.dxkit', 'policy.json');
|
|
190
|
+
const abs = path.join(cwd, rel);
|
|
191
|
+
let policy = {};
|
|
192
|
+
if (fs.existsSync(abs)) {
|
|
193
|
+
try {
|
|
194
|
+
policy = JSON.parse(fs.readFileSync(abs, 'utf8'));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
result.notes.push(`${rel} is not valid JSON — left untouched. Set loop.preset by hand.`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const existing = policy.loop?.preset;
|
|
202
|
+
// Override on explicit; else keep existing; else seed the default.
|
|
203
|
+
const target = explicit ?? existing ?? policy_1.DEFAULT_LOOP_PRESET;
|
|
204
|
+
if (existing === target) {
|
|
205
|
+
result.skipped.push(rel);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
policy.loop = { ...policy.loop, preset: target };
|
|
209
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
210
|
+
fs.writeFileSync(abs, JSON.stringify(policy, null, 2) + '\n', 'utf8');
|
|
211
|
+
result.installed.push(rel);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Scaffold the loop pack into `cwd`. Additive + idempotent: safe to run on
|
|
215
|
+
* a fresh repo or re-run on one that already has settings/CLAUDE.md/policy.
|
|
216
|
+
*/
|
|
217
|
+
function installClaudeLoop(cwd, opts = {}) {
|
|
218
|
+
const result = { installed: [], skipped: [], sidecars: [], notes: [] };
|
|
219
|
+
mergeStopHook(cwd, result);
|
|
220
|
+
upsertClaudeBlock(cwd, result);
|
|
221
|
+
ensureLoopPreset(cwd, opts.preset, result);
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=scaffold.js.map
|