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.
- package/bin/novahiz.mjs +30 -2
- package/dist/autodocs.js +170 -0
- package/dist/catalog.js +245 -0
- package/dist/classify.js +181 -0
- package/dist/cli.js +218 -0
- package/dist/commands/autodocs.js +218 -0
- package/dist/commands/clean.js +106 -0
- package/dist/commands/context.js +167 -0
- package/dist/commands/doctor.js +221 -0
- package/dist/commands/gate.js +218 -0
- package/dist/commands/graft.js +146 -0
- package/dist/commands/init.js +325 -0
- package/dist/commands/inspect.js +314 -0
- package/dist/commands/report.js +87 -0
- package/dist/commands/task.js +354 -0
- package/dist/commands/tokens.js +69 -0
- package/dist/complexity.js +265 -0
- package/dist/content.js +99 -0
- package/dist/db.js +176 -0
- package/dist/deps.js +24 -0
- package/dist/exec.js +108 -0
- package/dist/gate-repair.js +76 -0
- package/dist/gate.js +493 -0
- package/dist/graft.js +257 -0
- package/dist/ledger.js +431 -0
- package/dist/memory.js +441 -0
- package/dist/prompt-rewriter.js +481 -0
- package/dist/providers.js +47 -0
- package/dist/relevance.js +58 -0
- package/dist/render.js +90 -0
- package/dist/spec.js +208 -0
- package/dist/targets.js +375 -0
- package/install/bootstrap.mjs +25 -6
- package/install/install.mjs +32 -44
- package/install/lib.mjs +41 -0
- package/package.json +5 -3
- package/skills/novahiz-gate/SKILL.md +3 -3
- package/src/exec.ts +6 -1
- package/src/spec.ts +4 -1
|
@@ -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
|
+
}
|
package/dist/content.js
ADDED
|
@@ -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
|
+
}
|