novahiz 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Task complexity scorer — multi-dimension analysis.
3
+ *
4
+ * Uses 6 weighted dimensions inspired by NVIDIA's Prompt Task & Complexity
5
+ * Classifier and MICE (Multiple Independent Classifiers Ensemble):
6
+ *
7
+ * 1. Technical Depth (0.25) — code/domain terms, API references
8
+ * 2. Reasoning Depth (0.25) — analysis, comparison, evaluation, proof
9
+ * 3. Scope & Scale (0.20) — multi-file, codebase-wide, cross-cutting
10
+ * 4. Constraints (0.15) — requirements, dependencies, constraints count
11
+ * 5. Domain Specificity (0.10) — security, payments, database, design
12
+ * 6. Action Verb Intensity (0.05) — implement vs fix vs rename
13
+ *
14
+ * Three tiers:
15
+ * - trivial: typo fixes, comments, config changes → no pipeline
16
+ * - lite: bug fixes, minor additions → implement + converge only
17
+ * - full: features, refactors, audits → complete 6-step pipeline
18
+ */
19
+ // ── Signal banks by dimension ──────────────────────────────────────────────
20
+ const SIGNALS = {
21
+ technicalDepth: [
22
+ // Architecture & patterns
23
+ /\b(refactor|architecture|design\s*system|restructure|migrate|migration)\b/i,
24
+ /\b(microservice|monolith|scaling|horizontal|vertical\s*scal)\b/i,
25
+ /\b(ci\/cd|pipeline|deployment|infrastructure|terraform|kubernetes|docker)\b/i,
26
+ // API & protocols
27
+ /\b(api\s*design|rest\s*api|graphql|grpc|openapi|swagger|webhook|endpoint)\b/i,
28
+ /\b(websocket|socket\.io|server[\s-]?sent|event[\s-]?source|long[\s-]?polling)\b/i,
29
+ // Real-time & streaming
30
+ /\b(real[\s-]?time|streaming|live[\s-]?data|push[\s-]?notif|event[\s-]?driven)\b/i,
31
+ // Data structures & algorithms
32
+ /\b(algorithm|data\s*structure|tree|graph|hash|queue|stack|linked\s*list)\b/i,
33
+ // State management & patterns
34
+ /\b(state\s*management|redux|vuex|zustand|recoil|signal|observable|stream)\b/i,
35
+ // Build & tooling
36
+ /\b(webpack|vite|rollup|esbuild|babel|typescript\s*config|tsconfig)\b/i,
37
+ /\b(package[\s-]?json|dependency|dependency[\s-]?tree|transitive)\b/i,
38
+ // Memory & performance
39
+ /\b(memory\s*leak|performance|optimization|profiling|latency|throughput|bottleneck)\b/i,
40
+ // Concurrency
41
+ /\b(concurrent|parallel|race[\s-]?condition|deadlock|thread|async|await|mutex|semaphore)\b/i,
42
+ // Caching
43
+ /\b(cache|caching|redis|memcached|invalidation|ttl|eviction)\b/i,
44
+ ],
45
+ reasoningDepth: [
46
+ // Analysis verbs
47
+ /\b(analyze|analyse|investigate|explore|audit|review|inspect|examine|diagnose)\b/i,
48
+ // Comparison & evaluation
49
+ /\b(compare|evaluate|assess|benchmark|trade[\s-]?offs?|pros?\s*(and|&)\s*cons?)\b/i,
50
+ // Proof & verification
51
+ /\b(prove|verify|validate|confirm|demonstrate|show\s*that|argue)\b/i,
52
+ // Logical reasoning
53
+ /\b(deduce|infer|conclude|reason|logic|therefore|thus|hence)\b/i,
54
+ // Synthesis
55
+ /\b(synthesize|integrate|combine|merge|consolidate|unify)\b/i,
56
+ // Design thinking
57
+ /\b(design|architect|plan|strategize|prioritize|decompose|break\s*down)\b/i,
58
+ // Optimization reasoning
59
+ /\b(optimize|simplify|streamline|refactor|improve|enhance|elevate)\b/i,
60
+ // Multi-step reasoning
61
+ /\b(step[\s-]?by[\s-]?step|progressive|iterative|incremental|gradual)\b/i,
62
+ ],
63
+ scopeScale: [
64
+ // Cross-file scope
65
+ /\b(across|entire|all\s*files|multiple\s*files|codebase|project[\s-]?wide)\b/i,
66
+ /\b(full[\s-]?stack|end[\s-]?to[\s-]?end|e2e|top[\s-]?to[\s-]?bottom)\b/i,
67
+ // Setup & scaffolding
68
+ /\b(setup|scaffold|initialize|bootstrap|new\s*project|from\s*scratch)\b/i,
69
+ // Multi-component
70
+ /\b(components?|modules?|services?|layers?|tiers?|packages?)\s*(and|&|\+)\s*(components?|modules?|services?|layers?|tiers?|packages?)/i,
71
+ // Migration scope
72
+ /\b(migrate|upgrade|transition|convert|transform)\s+(all|every|entire|the\s*whole|from\s*\w+\s*to)\b/i,
73
+ // System-wide
74
+ /\b(system[\s-]?wide|globally|throughout|everywhere|universally)\b/i,
75
+ // Multi-step tasks
76
+ /\b(implement\s+a\s+(full|complete|comprehensive|new)|build\s+a\s+(full|complete|new))\b/i,
77
+ ],
78
+ constraints: [
79
+ // Explicit requirements
80
+ /\b(must|shall|required|mandatory|obligatory|necessary|essential)\b/i,
81
+ // Compatibility
82
+ /\b(compatible|backward|forward|cross[\s-]?browser|cross[\s-]?platform|support\s+(for|all|both|multiple))\b/i,
83
+ // Dependencies
84
+ /\b(depend|prerequisite|require|depends?\s*on|rely\s*on|reliance)\b/i,
85
+ // Security constraints
86
+ /\b(secure|security|encrypt|decrypt|auth|permission|access[\s-]?control|role[\s-]?based)\b/i,
87
+ // Performance constraints
88
+ /\b(performance|fast|slow|latency|timeout|throttle|rate[\s-]?limit|budget)\b/i,
89
+ // Compliance
90
+ /\b(compliant|compliance|regulate|gdpr|hipaa|soc\s*2|pci[\s-]?dss|owasp)\b/i,
91
+ // Time constraints
92
+ /\b(deadline|urgent|asap|quickly|immediate|soon|today|now)\b/i,
93
+ // Quality constraints
94
+ /\b(test|coverage|lint|clean|robust|resilient|fault[\s-]?tolerant|graceful)\b/i,
95
+ ],
96
+ domainSpecificity: [
97
+ // Security domain
98
+ /\b(security|vulnerability|owasp|cwe|xss|csrf|injection|sqli|penetration)\b/i,
99
+ // Payments domain
100
+ /\b(payment|stripe|checkout|subscription|billing|invoice|payment[\s-]?gateway)\b/i,
101
+ // Database domain
102
+ /\b(schema|table|column|index|foreign[\s-]?key|constraint|trigger|migration|supabase|postgres)\b/i,
103
+ // Auth domain
104
+ /\b(auth|authentication|authorization|oauth|jwt|session|token|login|signup)\b/i,
105
+ // Real-time domain
106
+ /\b(real[\s-]?time|streaming|queue|worker|cron|scheduled|pub[\s-]?sub)\b/i,
107
+ // Design domain
108
+ /\b(ui|ux|interface|landing[\s-]?page|mockup|wireframe|layout|responsive|a11y|animation)\b/i,
109
+ // Infrastructure domain
110
+ /\b(ci\/cd|pipeline|deployment|infrastructure|terraform|kubernetes|docker|cloud)\b/i,
111
+ // AI/ML domain
112
+ /\b(machine[\s-]?learning|ml|ai|neural|model|training|inference|embedding|vector)\b/i,
113
+ // Testing domain
114
+ /\b(test[\s-]?suite|integration[\s-]?test|e2e|end[\s-]?to[\s-]?end|coverage|mock|stub)\b/i,
115
+ ],
116
+ actionIntensity: [
117
+ // Full-tier actions (complex verbs)
118
+ { pattern: /\b(implement|build|create|design|architect|scaffold|bootstrap)\b/i, lite: 0, full: 3 },
119
+ { pattern: /\b(refactor|rewrite|restructure|reorganize|migrate|convert)\b/i, lite: 0, full: 3 },
120
+ { pattern: /\b(audit|security\s*review|pentest|penetration\s*test)\b/i, lite: 0, full: 3 },
121
+ { pattern: /\b(deploy|release|ship|launch|publish)\b/i, lite: 1, full: 1 },
122
+ // Lite-tier actions (repair verbs)
123
+ { pattern: /\b(fix|repair|patch|resolve|debug|diagnose)\b/i, lite: 2, full: 0 },
124
+ { pattern: /\b(update|upgrade|bump|change|modify|edit|adjust)\b/i, lite: 2, full: 0 },
125
+ { pattern: /\b(add|remove|delete|write|rename|relabel|reword|rephrase|optimize|simplify)\b/i, lite: 2, full: 0 },
126
+ // Trivial-tier actions (simple verbs)
127
+ { pattern: /\b(comment|document|annotate|explain)\b/i, lite: 0, full: 0 },
128
+ { pattern: /\b(typo|spelling|misspell|correct\s*the)\b/i, lite: 0, full: 0 },
129
+ ],
130
+ };
131
+ // ── Trivial signals (override to trivial) ──────────────────────────────────
132
+ const TRIVIAL_OVERRIDE = [
133
+ // Explicit typo/fix commands
134
+ /\b(fix\s+the\s+typo|correct\s+the\s+spelling|change\s+\w+\s+to\s+\w+)\b/i,
135
+ // Single-word commands
136
+ /^(rename|set|update|add|remove|delete|toggle|enable|disable)\s+\S+(\s+\S+){0,2}$/i,
137
+ // Config-only changes — NOT trivial: changing config is a real action
138
+ // /\b(update\s+the\s+config|change\s+the\s+setting|bump\s+the\s+version)\b/i,
139
+ // Comment-only — NOT trivial: "add a comment" is a real action
140
+ // /\b(add\s+a\s+comment|document\s+the|add\s+docstring)\b/i,
141
+ ];
142
+ // ── Scoring functions ──────────────────────────────────────────────────────
143
+ function countMatches(text, patterns) {
144
+ let count = 0;
145
+ for (const p of patterns) {
146
+ if (p.test(text))
147
+ count++;
148
+ }
149
+ return count;
150
+ }
151
+ function scoreActionIntensity(text) {
152
+ let lite = 0;
153
+ let full = 0;
154
+ for (const { pattern, lite: l, full: f } of SIGNALS.actionIntensity) {
155
+ if (pattern.test(text)) {
156
+ lite += l;
157
+ full += f;
158
+ }
159
+ }
160
+ return { lite, full };
161
+ }
162
+ function countConstraints(text) {
163
+ let count = 0;
164
+ for (const p of SIGNALS.constraints) {
165
+ if (p.test(text))
166
+ count++;
167
+ }
168
+ return count;
169
+ }
170
+ // ── Main scoring ───────────────────────────────────────────────────────────
171
+ export function scoreDimensions(prompt) {
172
+ const text = prompt.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
173
+ const technicalDepth = countMatches(text, SIGNALS.technicalDepth);
174
+ const reasoningDepth = countMatches(text, SIGNALS.reasoningDepth);
175
+ const scopeScale = countMatches(text, SIGNALS.scopeScale);
176
+ const constraints = countConstraints(text);
177
+ const domainSpecificity = countMatches(text, SIGNALS.domainSpecificity);
178
+ const action = scoreActionIntensity(text);
179
+ // Normalize constraint count (cap at 5 for scoring purposes)
180
+ const normalizedConstraints = Math.min(constraints, 5);
181
+ return {
182
+ technicalDepth,
183
+ reasoningDepth,
184
+ scopeScale,
185
+ constraints: normalizedConstraints,
186
+ domainSpecificity,
187
+ actionIntensity: { lite: action.lite, full: action.full },
188
+ };
189
+ }
190
+ /**
191
+ * Compute complexity scores using additive model.
192
+ * Each dimension match adds points. Higher total = more complex.
193
+ */
194
+ function computeScores(dims, wordCount) {
195
+ // Full-tier score: sum of all dimension matches (each match = 1 point)
196
+ // Plus action intensity bonus for creation/implementation verbs
197
+ const full = dims.technicalDepth +
198
+ dims.reasoningDepth +
199
+ dims.scopeScale +
200
+ dims.constraints +
201
+ dims.domainSpecificity +
202
+ dims.actionIntensity.full;
203
+ // Lite-tier score: repair actions + low-complexity signals
204
+ const lite = dims.actionIntensity.lite +
205
+ dims.technicalDepth +
206
+ dims.reasoningDepth +
207
+ dims.domainSpecificity;
208
+ // Trivial bonus: short prompts with no complexity signals
209
+ const trivial = wordCount <= 5 ? 3 : wordCount <= 8 ? 1 : 0;
210
+ return { full, lite, trivial };
211
+ }
212
+ /**
213
+ * Score a prompt and determine its complexity tier.
214
+ */
215
+ export function scoreComplexity(prompt) {
216
+ // Check trivial override first
217
+ const normalized = prompt.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
218
+ for (const pattern of TRIVIAL_OVERRIDE) {
219
+ if (pattern.test(normalized))
220
+ return "trivial";
221
+ }
222
+ const dims = scoreDimensions(prompt);
223
+ const wordCount = normalized.trim().split(/\s+/).length;
224
+ const { full, lite, trivial } = computeScores(dims, wordCount);
225
+ // Decision logic:
226
+ // full >= 3 → definitely full
227
+ // full >= 2 → full (2+ dimension matches is substantial)
228
+ // dims.actionIntensity.full >= 2 && wordCount >= 5 → full (creation verb in a non-trivial prompt)
229
+ // full >= 1 && wordCount >= 12 → full (long prompt with at least 1 signal)
230
+ // dims.actionIntensity.lite >= 2 && wordCount >= 3 → lite (repair verb in a non-trivial prompt)
231
+ // trivial >= 3 && full < 2 → trivial (short, no complexity, no repair verb)
232
+ // lite >= 2 && full < 2 → lite
233
+ // default: use length heuristic
234
+ if (full >= 3)
235
+ return "full";
236
+ if (full >= 2)
237
+ return "full";
238
+ if (dims.actionIntensity.full >= 2 && wordCount >= 5)
239
+ return "full";
240
+ if (full >= 1 && wordCount >= 12)
241
+ return "full";
242
+ if (dims.actionIntensity.lite >= 2 && wordCount >= 3)
243
+ return "lite";
244
+ if (trivial >= 3 && full < 2)
245
+ return "trivial";
246
+ if (lite >= 2 && full < 2)
247
+ return "lite";
248
+ // Default by length — H5: only trigger "full" if technical signals are present
249
+ if (wordCount <= 8)
250
+ return "trivial";
251
+ if (wordCount <= 40)
252
+ return "lite";
253
+ const hasDomainSignal = dims.technicalDepth > 0 || dims.domainSpecificity > 0;
254
+ return hasDomainSignal ? "full" : "lite";
255
+ }
256
+ /**
257
+ * Determine the complexity tier based on prompt analysis.
258
+ * Main entry point for the complexity system.
259
+ */
260
+ export function determineTier(prompt) {
261
+ // C2: guard against null/undefined — treat as trivial instead of crashing
262
+ if (!prompt || typeof prompt !== "string")
263
+ return "trivial";
264
+ return scoreComplexity(prompt);
265
+ }
@@ -0,0 +1,99 @@
1
+ export function changeText(tool, args) {
2
+ const record = (args && typeof args === "object" ? args : {});
3
+ const parts = [];
4
+ for (const key of ["newString", "new_string", "content", "patchText", "patch", "newText"]) {
5
+ const value = record[key];
6
+ if (typeof value === "string" && value.length > 0)
7
+ parts.push(value);
8
+ }
9
+ const edits = record.edits;
10
+ if (Array.isArray(edits)) {
11
+ for (const edit of edits) {
12
+ if (!edit || typeof edit !== "object")
13
+ continue;
14
+ const entry = edit;
15
+ for (const key of ["newString", "new_string", "content", "newText"]) {
16
+ const value = entry[key];
17
+ if (typeof value === "string" && value.length > 0)
18
+ parts.push(value);
19
+ }
20
+ }
21
+ }
22
+ return parts.join("\n");
23
+ }
24
+ const STYLE_SIGNALS = [
25
+ /(^|\n)\s*[.#][\w-]+(\s*[:>+~]\s*[.#\w:-]+)*\s*[,{]/,
26
+ /className\s*=/i,
27
+ /style\s*[=:]\s*[{"]/i,
28
+ /styled\./,
29
+ /\bcss`/,
30
+ /\b(gap-\d|p[xy]?-\d|m[xy]?-\d|text-\w+-\d|bg-\w+-\d)\b/i
31
+ ];
32
+ const PROSE_SIGNALS = [
33
+ /\/\*\*?[\s\S]*?[A-Za-z][A-Za-z ,.'()-]{12,}[\s\S]*?\*\//,
34
+ /"[^"\n]{15,}[ ][^"\n]{10,}"/,
35
+ /'[^'\n]{15,}[ ][^'\n]{10,}'/,
36
+ /`[^`\n]{15,}[ ][^`\n]{10,}`/,
37
+ /(^|\n)\s{0,3}\S[^\n]*\s\S+\s[^\n]*[.!?](\s|$)/
38
+ ];
39
+ // H6: the old single-line rule — any `//`/`#` comment with 12+ alpha chars —
40
+ // flagged license headers ("Copyright 2024 ..."), TODOs and linter directives
41
+ // as prose. A comment is prose only when it reads like a sentence: no code
42
+ // tokens, no directive prefix, and either ending punctuation or a long
43
+ // multi-word description.
44
+ const COMMENT_DIRECTIVE = /^\s*(\/\/|#|\*)\s*(TODO|FIXME|XXX|HACK|eslint|tslint|prettier|stylelint|c8|istanbul|@ts-|@vite-|node:|deno-lint)/i;
45
+ const CODE_TOKENS = /[=;{}()[\]=>]/;
46
+ function isProseCommentLine(line) {
47
+ const match = line.match(/^\s*(\/\/|#|\*)\s+(.+)$/);
48
+ if (!match)
49
+ return false;
50
+ const body = match[2].trim();
51
+ if (body.length === 0)
52
+ return false;
53
+ if (COMMENT_DIRECTIVE.test(line))
54
+ return false;
55
+ if (CODE_TOKENS.test(body))
56
+ return false;
57
+ // Complete sentence: uppercase start, ending punctuation.
58
+ if (/^[A-Z].{15,}[.!?]$/.test(body))
59
+ return true;
60
+ // Long multi-word description: 40+ chars, 4+ plain words.
61
+ if (body.length >= 40) {
62
+ const words = body.split(/\s+/).filter((word) => /^[A-Za-z][A-Za-z,.'()-]*$/.test(word));
63
+ return words.length >= 4;
64
+ }
65
+ return false;
66
+ }
67
+ export function hasStyle(text) {
68
+ if (text.length === 0)
69
+ return false;
70
+ return STYLE_SIGNALS.some((pattern) => pattern.test(text));
71
+ }
72
+ export function hasProse(text) {
73
+ if (text.length === 0)
74
+ return false;
75
+ if (PROSE_SIGNALS.some((pattern) => pattern.test(text)))
76
+ return true;
77
+ return text.split(/\r?\n/).some(isProseCommentLine);
78
+ }
79
+ export function isTrivial(text, minChange = 0) {
80
+ if (minChange <= 0)
81
+ return text.trim().length === 0;
82
+ return text.replace(/\s+/g, " ").trim().length < minChange;
83
+ }
84
+ const PLACEHOLDER_PATTERNS = [
85
+ /\b(TODO|FIXME|XXX|HACK)\b/,
86
+ /\bnot implemented\b/i,
87
+ /\bcoming soon\b/i,
88
+ /<placeholder/i,
89
+ /<your[_ ]code/i,
90
+ /\blorem ipsum\b/i,
91
+ /\.\.\.\s*rest of\b/i,
92
+ /\bimplement me\b/i,
93
+ /\.\.\.\s*existing\b/i
94
+ ];
95
+ export function hasPlaceholder(text) {
96
+ if (text.length === 0)
97
+ return false;
98
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(text));
99
+ }
package/dist/db.js ADDED
@@ -0,0 +1,176 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { chmodSync, mkdirSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { pruneSessions } from "./ledger.js";
5
+ import { autoCommit } from "./graft.js";
6
+ export { DatabaseSync };
7
+ export function openDb(dbPath) {
8
+ const absolute = resolve(dbPath);
9
+ mkdirSync(dirname(absolute), { recursive: true });
10
+ const db = new DatabaseSync(absolute);
11
+ if (process.platform !== "win32") {
12
+ try {
13
+ chmodSync(absolute, 0o600);
14
+ }
15
+ catch {
16
+ // best effort, the umask may forbid it
17
+ }
18
+ }
19
+ db.exec("PRAGMA journal_mode = WAL;");
20
+ db.exec("PRAGMA busy_timeout = 5000;");
21
+ db.exec("PRAGMA foreign_keys = ON;");
22
+ db.exec(`
23
+ CREATE TABLE IF NOT EXISTS meta (
24
+ key TEXT PRIMARY KEY,
25
+ value TEXT
26
+ );
27
+ CREATE TABLE IF NOT EXISTS skills (
28
+ id TEXT PRIMARY KEY,
29
+ name TEXT NOT NULL,
30
+ description TEXT,
31
+ source_path TEXT,
32
+ power INTEGER NOT NULL DEFAULT 3,
33
+ stars INTEGER,
34
+ tags TEXT NOT NULL DEFAULT '[]',
35
+ categories TEXT NOT NULL DEFAULT '[]',
36
+ scanned_at TEXT NOT NULL
37
+ );
38
+ CREATE TABLE IF NOT EXISTS categories (
39
+ id TEXT PRIMARY KEY,
40
+ label TEXT,
41
+ priority INTEGER NOT NULL DEFAULT 0,
42
+ keywords TEXT NOT NULL DEFAULT '[]',
43
+ default_skills TEXT NOT NULL DEFAULT '[]'
44
+ );
45
+ CREATE TABLE IF NOT EXISTS rules (
46
+ id TEXT PRIMARY KEY,
47
+ json TEXT NOT NULL
48
+ );
49
+ CREATE TABLE IF NOT EXISTS sessions (
50
+ id TEXT PRIMARY KEY,
51
+ agent TEXT,
52
+ categories TEXT NOT NULL DEFAULT '[]',
53
+ required_skills TEXT NOT NULL DEFAULT '[]',
54
+ updated_at TEXT NOT NULL
55
+ );
56
+ CREATE TABLE IF NOT EXISTS skill_invocations (
57
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
58
+ skill TEXT NOT NULL,
59
+ invoked_at TEXT NOT NULL,
60
+ PRIMARY KEY (session_id, skill)
61
+ );
62
+ CREATE TABLE IF NOT EXISTS roadmap_progress (
63
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
64
+ step_id TEXT NOT NULL,
65
+ status TEXT NOT NULL DEFAULT 'done',
66
+ updated_at TEXT NOT NULL,
67
+ PRIMARY KEY (session_id, step_id)
68
+ );
69
+ CREATE TABLE IF NOT EXISTS enforcement_log (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ session_id TEXT,
72
+ tool TEXT,
73
+ file_path TEXT,
74
+ file_class TEXT,
75
+ decision TEXT,
76
+ missing TEXT NOT NULL DEFAULT '[]',
77
+ matched_rules TEXT NOT NULL DEFAULT '[]',
78
+ logged_at TEXT NOT NULL
79
+ );
80
+ CREATE TABLE IF NOT EXISTS tasks (
81
+ id TEXT PRIMARY KEY,
82
+ title TEXT NOT NULL,
83
+ status TEXT NOT NULL DEFAULT 'active',
84
+ session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
85
+ created_at TEXT NOT NULL,
86
+ updated_at TEXT NOT NULL,
87
+ revision INTEGER NOT NULL DEFAULT 0,
88
+ reviewed_at TEXT,
89
+ edits_since_review INTEGER NOT NULL DEFAULT 0,
90
+ todos_since_review INTEGER NOT NULL DEFAULT 0
91
+ );
92
+ CREATE TABLE IF NOT EXISTS todos (
93
+ id TEXT PRIMARY KEY,
94
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
95
+ seq INTEGER NOT NULL DEFAULT 0,
96
+ label TEXT NOT NULL,
97
+ kind TEXT NOT NULL DEFAULT 'edit',
98
+ status TEXT NOT NULL DEFAULT 'pending',
99
+ acceptance TEXT,
100
+ proof TEXT,
101
+ owner TEXT,
102
+ depends_on TEXT NOT NULL DEFAULT '[]',
103
+ iterations INTEGER NOT NULL DEFAULT 0,
104
+ max_iterations INTEGER,
105
+ updated_at TEXT NOT NULL
106
+ );
107
+ CREATE INDEX IF NOT EXISTS enforcement_log_logged_at ON enforcement_log(logged_at);
108
+ CREATE INDEX IF NOT EXISTS enforcement_log_session ON enforcement_log(session_id);
109
+ CREATE INDEX IF NOT EXISTS todos_task_id ON todos(task_id);
110
+ CREATE INDEX IF NOT EXISTS tasks_status_session ON tasks(status, session_id);
111
+ CREATE INDEX IF NOT EXISTS skill_invocations_session ON skill_invocations(session_id);
112
+ CREATE INDEX IF NOT EXISTS roadmap_progress_session ON roadmap_progress(session_id);
113
+ `);
114
+ migrate(db);
115
+ // H4: enforce the session TTL on every open — best effort, prune failures
116
+ // must never break startup.
117
+ try {
118
+ pruneSessions(db);
119
+ // Auto-commit after session pruning
120
+ autoCommit("prune-sessions");
121
+ }
122
+ catch {
123
+ // sessions table may predate updated_at on very old installs; migrate covers it
124
+ }
125
+ return db;
126
+ }
127
+ // Table and column names are interpolated here, unlike every other query in the
128
+ // project, because SQLite does not parameterize identifiers. H8: the allowlist
129
+ // below makes the "fixed strings only" invariant structural — any future caller
130
+ // passing a non-listed identifier throws instead of injecting SQL.
131
+ const SAFE_TABLES = new Set([
132
+ "meta",
133
+ "skills",
134
+ "categories",
135
+ "rules",
136
+ "sessions",
137
+ "skill_invocations",
138
+ "roadmap_progress",
139
+ "enforcement_log",
140
+ "tasks",
141
+ "todos"
142
+ ]);
143
+ const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
144
+ function assertSafeIdentifier(kind, value) {
145
+ if (!SAFE_IDENTIFIER.test(value))
146
+ throw new Error(`unsafe SQL ${kind}: ${value}`);
147
+ }
148
+ function tableColumns(db, table) {
149
+ if (!SAFE_TABLES.has(table))
150
+ throw new Error(`unexpected table name: ${table}`);
151
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
152
+ return new Set(rows.map((row) => row.name));
153
+ }
154
+ function ensureColumn(db, table, column, definition) {
155
+ if (!SAFE_TABLES.has(table))
156
+ throw new Error(`unexpected table name: ${table}`);
157
+ assertSafeIdentifier("column", column);
158
+ if (tableColumns(db, table).has(column))
159
+ return;
160
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
161
+ }
162
+ export const SCHEMA_VERSION = 1;
163
+ function migrate(db) {
164
+ ensureColumn(db, "tasks", "revision", "INTEGER NOT NULL DEFAULT 0");
165
+ ensureColumn(db, "tasks", "reviewed_at", "TEXT");
166
+ ensureColumn(db, "tasks", "edits_since_review", "INTEGER NOT NULL DEFAULT 0");
167
+ ensureColumn(db, "tasks", "todos_since_review", "INTEGER NOT NULL DEFAULT 0");
168
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
169
+ }
170
+ export function setMeta(db, key, value) {
171
+ db.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
172
+ }
173
+ export function getMeta(db, key) {
174
+ const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
175
+ return row?.value ?? null;
176
+ }
package/dist/deps.js ADDED
@@ -0,0 +1,24 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export function commandExists(command) {
3
+ const probe = process.platform === "win32" ? "where" : "which";
4
+ const result = spawnSync(probe, [command], { encoding: "utf8", shell: false });
5
+ return result.status === 0;
6
+ }
7
+ export function checkDependencies(spec) {
8
+ return spec.providers.map((provider) => {
9
+ const requires = provider.requires ?? [];
10
+ const missing = requires.filter((command) => !commandExists(command));
11
+ return { id: provider.id, kind: provider.kind, requires, missing, ok: missing.length === 0 };
12
+ });
13
+ }
14
+ export function bootstrapFor(provider, platform = process.platform) {
15
+ const bootstrap = provider.bootstrap;
16
+ if (!bootstrap)
17
+ return null;
18
+ return bootstrap[platform] ?? bootstrap.default ?? null;
19
+ }
20
+ export function missingPrerequisites(spec) {
21
+ return spec.providers
22
+ .map((provider) => ({ provider, missing: (provider.requires ?? []).filter((command) => !commandExists(command)) }))
23
+ .filter((entry) => entry.missing.length > 0);
24
+ }
package/dist/exec.js ADDED
@@ -0,0 +1,108 @@
1
+ import { spawnSync } from "node:child_process";
2
+ // Allowed characters for a single argument. It excludes every character a shell
3
+ // can reinterpret: whitespace, & | < > ^ % ! ( ) " ' ` ; $ * ? and newlines.
4
+ // The Windows branch below joins tokens with spaces and hands the result to
5
+ // cmd.exe, so widening this set would re-open command injection on that path.
6
+ const SAFE_TOKEN = /^[A-Za-z0-9@._+,/:=~-]+$/;
7
+ function resultFrom(status, stdout, stderr, error) {
8
+ return { ok: status === 0, status, stdout, stderr, error };
9
+ }
10
+ function refused(token) {
11
+ return { ok: false, status: null, stdout: "", stderr: "", error: `refused unsafe token: ${token}` };
12
+ }
13
+ export function unsafeToken(tokens) {
14
+ for (const token of tokens) {
15
+ if (token.length === 0)
16
+ return "";
17
+ if (!SAFE_TOKEN.test(token))
18
+ return token;
19
+ }
20
+ return null;
21
+ }
22
+ export function runCommand(bin, args) {
23
+ const bad = unsafeToken([bin, ...args]);
24
+ if (bad !== null)
25
+ return refused(bad);
26
+ const [command, spawnArgs] = resolveSpawn(bin, args);
27
+ const result = spawnSync(command, spawnArgs, { encoding: "utf8", windowsHide: true, shell: false });
28
+ return resultFrom(result.status, result.stdout ?? "", result.stderr ?? "", result.error?.message);
29
+ }
30
+ function resolveSpawn(bin, args) {
31
+ if (process.platform !== "win32")
32
+ return [bin, args];
33
+ // The shell is taken from the environment. Whoever can set ComSpec can already
34
+ // run code locally, so the token allowlist above is what actually contains the
35
+ // arguments; this is noted for completeness, not treated as a boundary.
36
+ const shell = process.env.ComSpec ?? "cmd.exe";
37
+ return [shell, ["/d", "/s", "/c", [bin, ...args].join(" ")]];
38
+ }
39
+ // Bootstrap may only start a known interpreter or packager. Shell binaries
40
+ // (sh, bash, pwsh, powershell, cmd) are deliberately absent: arguments are
41
+ // parsed by the executable itself, so a shell would turn this allowlist into
42
+ // a formality by reinterpreting anything it is handed.
43
+ const SAFE_BOOTSTRAP_BINS = new Set([
44
+ "node", "npm", "npx", "uv", "uvx", "python", "py"
45
+ ]);
46
+ function normalizeBin(bin) {
47
+ const base = bin.replace(/\\/g, "/").split("/").pop() ?? bin;
48
+ return base.toLowerCase();
49
+ }
50
+ function bootstrapBinAllowed(bin) {
51
+ const name = normalizeBin(bin);
52
+ const withoutExt = name.replace(/\.(exe|cmd|bat)$/i, "");
53
+ return SAFE_BOOTSTRAP_BINS.has(name) || SAFE_BOOTSTRAP_BINS.has(withoutExt);
54
+ }
55
+ function refusedBootstrap(bin) {
56
+ return { ok: false, status: null, stdout: "", stderr: "", error: `refused disallowed bootstrap binary: ${bin}` };
57
+ }
58
+ // Bootstrap argv comes from catalog/providers.json. Every token is bounded:
59
+ // the bin must be in SAFE_BOOTSTRAP_BINS, each argument must pass SAFE_TOKEN
60
+ // (no whitespace, no shell metacharacter), and interpreters that execute an
61
+ // inline string must not receive one — `python -c` and `node -e` are the
62
+ // shell -c equivalents that would otherwise nullify the allowlist.
63
+ const CODE_RUNNER_FLAGS = {
64
+ python: ["-c", "--command"],
65
+ py: ["-c", "--command"],
66
+ node: ["-e", "--eval", "-p", "--print", "--repl"]
67
+ };
68
+ export function isCodeRunnerFlag(bin, token) {
69
+ const name = normalizeBin(bin).replace(/\.(exe|cmd|bat)$/i, "");
70
+ const flags = CODE_RUNNER_FLAGS[name];
71
+ if (!flags)
72
+ return false;
73
+ if (token.startsWith("--")) {
74
+ const eq = token.indexOf("=");
75
+ return flags.includes(eq > 0 ? token.slice(0, eq) : token);
76
+ }
77
+ // A short option may carry its value attached: -ccode, -ecode.
78
+ if (token.startsWith("-") && token.length > 2)
79
+ return flags.includes(token.slice(0, 2));
80
+ return flags.includes(token);
81
+ }
82
+ function refusedCodeRunner(token) {
83
+ return { ok: false, status: null, stdout: "", stderr: "", error: `refused code-runner flag: ${token}` };
84
+ }
85
+ export function runScript(argv) {
86
+ if (argv.length === 0)
87
+ return refused("");
88
+ const [bin, ...args] = argv;
89
+ if (!SAFE_TOKEN.test(bin))
90
+ return refused(bin);
91
+ if (!bootstrapBinAllowed(bin))
92
+ return refusedBootstrap(bin);
93
+ for (const arg of args) {
94
+ if (arg.length === 0)
95
+ return refused("");
96
+ if (isCodeRunnerFlag(bin, arg))
97
+ return refusedCodeRunner(arg);
98
+ if (!SAFE_TOKEN.test(arg))
99
+ return refused(arg);
100
+ }
101
+ // Tokens are already bounded (SAFE_TOKEN: no whitespace, no metacharacter),
102
+ // so handing the joined string to cmd.exe on Windows is safe and is the only
103
+ // way npm/npx (.cmd shims) can run at all — direct CreateProcess throws
104
+ // ENOENT since the Node CVE-2024-* .cmd hardening.
105
+ const [command, spawnArgs] = resolveSpawn(bin, args);
106
+ const result = spawnSync(command, spawnArgs, { encoding: "utf8", windowsHide: true, shell: false });
107
+ return resultFrom(result.status, result.stdout ?? "", result.stderr ?? "", result.error?.message);
108
+ }