@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +246 -287
  3. package/dist/allowlist/hint.d.ts +1 -1
  4. package/dist/allowlist/hint.d.ts.map +1 -1
  5. package/dist/allowlist/hint.js +6 -3
  6. package/dist/allowlist/hint.js.map +1 -1
  7. package/dist/baseline/check.d.ts +7 -0
  8. package/dist/baseline/check.d.ts.map +1 -1
  9. package/dist/baseline/check.js +3 -1
  10. package/dist/baseline/check.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +101 -14
  13. package/dist/cli.js.map +1 -1
  14. package/dist/dashboard/graph-tab.d.ts.map +1 -1
  15. package/dist/dashboard/graph-tab.js +6 -3
  16. package/dist/dashboard/graph-tab.js.map +1 -1
  17. package/dist/doctor.d.ts.map +1 -1
  18. package/dist/doctor.js +13 -12
  19. package/dist/doctor.js.map +1 -1
  20. package/dist/generator.d.ts.map +1 -1
  21. package/dist/generator.js +8 -2
  22. package/dist/generator.js.map +1 -1
  23. package/dist/issue-cli.d.ts +1 -1
  24. package/dist/issue-cli.js +1 -1
  25. package/dist/loop/demo.d.ts +12 -0
  26. package/dist/loop/demo.d.ts.map +1 -0
  27. package/dist/loop/demo.js +331 -0
  28. package/dist/loop/demo.js.map +1 -0
  29. package/dist/loop/doctor.d.ts +37 -0
  30. package/dist/loop/doctor.d.ts.map +1 -0
  31. package/dist/loop/doctor.js +320 -0
  32. package/dist/loop/doctor.js.map +1 -0
  33. package/dist/loop/ledger-cli.d.ts +7 -0
  34. package/dist/loop/ledger-cli.d.ts.map +1 -0
  35. package/dist/loop/ledger-cli.js +95 -0
  36. package/dist/loop/ledger-cli.js.map +1 -0
  37. package/dist/loop/ledger.d.ts +95 -0
  38. package/dist/loop/ledger.d.ts.map +1 -0
  39. package/dist/loop/ledger.js +201 -0
  40. package/dist/loop/ledger.js.map +1 -0
  41. package/dist/loop/policy.d.ts +35 -0
  42. package/dist/loop/policy.d.ts.map +1 -0
  43. package/dist/loop/policy.js +151 -0
  44. package/dist/loop/policy.js.map +1 -0
  45. package/dist/loop/scaffold.d.ts +28 -0
  46. package/dist/loop/scaffold.d.ts.map +1 -0
  47. package/dist/loop/scaffold.js +224 -0
  48. package/dist/loop/scaffold.js.map +1 -0
  49. package/dist/loop/stop-gate.d.ts +71 -0
  50. package/dist/loop/stop-gate.d.ts.map +1 -0
  51. package/dist/loop/stop-gate.js +295 -0
  52. package/dist/loop/stop-gate.js.map +1 -0
  53. package/dist/self-invocation.d.ts +77 -0
  54. package/dist/self-invocation.d.ts.map +1 -0
  55. package/dist/self-invocation.js +157 -0
  56. package/dist/self-invocation.js.map +1 -0
  57. package/dist/ship-installers.d.ts.map +1 -1
  58. package/dist/ship-installers.js +8 -0
  59. package/dist/ship-installers.js.map +1 -1
  60. package/dist/types.d.ts +4 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/update.d.ts.map +1 -1
  63. package/dist/update.js +22 -5
  64. package/dist/update.js.map +1 -1
  65. package/dist/upgrade.d.ts +3 -3
  66. package/dist/upgrade.d.ts.map +1 -1
  67. package/dist/upgrade.js +5 -4
  68. package/dist/upgrade.js.map +1 -1
  69. package/package.json +6 -4
  70. package/templates/.claude/skills/dxkit-config/SKILL.md +17 -0
  71. package/templates/.claude/skills/dxkit-init/SKILL.md +1 -0
  72. package/templates/.claude/skills/dxkit-learn/SKILL.md +17 -0
  73. package/templates/.claude/skills/dxkit-loop/SKILL.md +114 -0
  74. package/templates/.claude/skills/dxkit-onboard/SKILL.md +2 -0
  75. 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