@njeybe/nativ 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +26 -0
- package/dist/commands/db/db-formatter.d.ts +5 -0
- package/dist/commands/db/db-formatter.js +116 -0
- package/dist/commands/db/db-introspect.d.ts +11 -0
- package/dist/commands/db/db-introspect.js +84 -0
- package/dist/commands/db.js +4 -206
- package/dist/commands/profile.d.ts +3 -0
- package/dist/commands/profile.js +39 -0
- package/dist/commands/task/task-add.d.ts +11 -0
- package/dist/commands/task/task-add.js +169 -0
- package/dist/commands/task/task-common.d.ts +57 -0
- package/dist/commands/task/task-common.js +45 -0
- package/dist/commands/task/task-escalate.d.ts +7 -0
- package/dist/commands/task/task-escalate.js +119 -0
- package/dist/commands/task/task-lifecycle.d.ts +5 -0
- package/dist/commands/task/task-lifecycle.js +252 -0
- package/dist/commands/task/task-query.d.ts +10 -0
- package/dist/commands/task/task-query.js +269 -0
- package/dist/commands/task.d.ts +9 -87
- package/dist/commands/task.js +8 -850
- package/dist/commands/triage/triage-store.d.ts +56 -0
- package/dist/commands/triage/triage-store.js +108 -0
- package/dist/commands/triage/triage-ui.d.ts +14 -0
- package/dist/commands/triage/triage-ui.js +104 -0
- package/dist/commands/triage.d.ts +3 -52
- package/dist/commands/triage.js +8 -216
- package/dist/core/enforcement.d.ts +1 -0
- package/dist/core/enforcement.js +19 -0
- package/dist/core/human-gate.d.ts +2 -1
- package/dist/core/human-gate.js +9 -2
- package/dist/core/profiles.d.ts +14 -0
- package/dist/core/profiles.js +82 -0
- package/dist/core/setup/config-merger.d.ts +59 -0
- package/dist/core/setup/config-merger.js +107 -0
- package/dist/core/setup/session-context.d.ts +4 -0
- package/dist/core/setup/session-context.js +65 -0
- package/dist/core/setup/setup-planner.d.ts +14 -0
- package/dist/core/setup/setup-planner.js +180 -0
- package/dist/core/setup/template-planner.d.ts +8 -0
- package/dist/core/setup/template-planner.js +32 -0
- package/dist/core/setup/types.d.ts +65 -0
- package/dist/core/setup/types.js +55 -0
- package/dist/core/setup-assets.d.ts +8 -119
- package/dist/core/setup-assets.js +7 -437
- package/dist/core/spec-slices.js +119 -7
- package/dist/core/tier1-liaison.d.ts +2 -0
- package/dist/core/tier1-liaison.js +64 -3
- package/dist/core/verifier/executor.d.ts +5 -0
- package/dist/core/verifier/executor.js +62 -0
- package/dist/core/verifier/phases.d.ts +5 -0
- package/dist/core/verifier/phases.js +71 -0
- package/dist/core/verifier/task-verifier.d.ts +37 -0
- package/dist/core/verifier/task-verifier.js +233 -0
- package/dist/core/verifier/types.d.ts +51 -0
- package/dist/core/verifier/types.js +1 -0
- package/dist/core/verifier.d.ts +5 -93
- package/dist/core/verifier.js +5 -359
- package/dist/db/introspector.js +59 -59
- package/dist/governor/evaluator.d.ts +4 -64
- package/dist/governor/evaluator.js +21 -686
- package/dist/governor/patch-applier.d.ts +8 -0
- package/dist/governor/patch-applier.js +121 -0
- package/dist/governor/self-healing.d.ts +24 -0
- package/dist/governor/self-healing.js +351 -0
- package/dist/governor/test-integrity.d.ts +41 -0
- package/dist/governor/test-integrity.js +237 -0
- package/dist/index.js +8 -0
- package/dist/mcp/capture.d.ts +4 -0
- package/dist/mcp/capture.js +33 -0
- package/dist/mcp/resources.d.ts +39 -0
- package/dist/mcp/resources.js +44 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +14 -89
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/runner/agent-supervisor.d.ts +8 -302
- package/dist/runner/agent-supervisor.js +41 -1030
- package/dist/runner/cli-config.d.ts +84 -0
- package/dist/runner/cli-config.js +215 -0
- package/dist/runner/native-driver.d.ts +18 -0
- package/dist/runner/native-driver.js +208 -0
- package/dist/runner/native-engine.d.ts +27 -0
- package/dist/runner/native-engine.js +184 -0
- package/dist/runner/native-tools.d.ts +31 -0
- package/dist/runner/native-tools.js +241 -0
- package/dist/runner/process-utils.d.ts +12 -0
- package/dist/runner/process-utils.js +96 -0
- package/dist/runner/run-lifecycle.d.ts +6 -0
- package/dist/runner/run-lifecycle.js +42 -0
- package/dist/runner/types.d.ts +215 -0
- package/dist/runner/types.js +41 -0
- package/dist/scanner/detector.js +42 -663
- package/dist/scanner/detectors/ecosystem-detector.d.ts +16 -0
- package/dist/scanner/detectors/ecosystem-detector.js +354 -0
- package/dist/scanner/detectors/orm-detector.d.ts +5 -0
- package/dist/scanner/detectors/orm-detector.js +59 -0
- package/dist/scanner/detectors/workspace-detector.d.ts +19 -0
- package/dist/scanner/detectors/workspace-detector.js +266 -0
- package/dist/server/cli-runner.d.ts +15 -0
- package/dist/server/cli-runner.js +43 -0
- package/dist/server/event-hub.d.ts +42 -0
- package/dist/server/event-hub.js +169 -0
- package/dist/server/http-utils.d.ts +15 -0
- package/dist/server/http-utils.js +56 -0
- package/dist/server/plan-utils.d.ts +9 -0
- package/dist/server/plan-utils.js +31 -0
- package/dist/server/routes/db-routes.d.ts +57 -0
- package/dist/server/routes/db-routes.js +484 -0
- package/dist/server/routes/task-routes.d.ts +266 -0
- package/dist/server/routes/task-routes.js +285 -0
- package/dist/server/routes/telemetry-routes.d.ts +128 -0
- package/dist/server/routes/telemetry-routes.js +179 -0
- package/dist/server/routes/triage-routes.d.ts +81 -0
- package/dist/server/routes/triage-routes.js +244 -0
- package/dist/server/routes/worktree-routes.d.ts +31 -0
- package/dist/server/routes/worktree-routes.js +144 -0
- package/dist/server/studio-server.d.ts +4 -11
- package/dist/server/studio-server.js +34 -1578
- package/dist/server/studio-ui/shell/pipeline-state.js +2 -0
- package/dist/server/studio-ui/shell/triage.js +10 -0
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +4 -2
- package/plugin/agents/verifier.md +1 -1
- package/plugin/agents/worker.md +7 -4
- package/plugin/skills/nativ/SKILL.md +1 -0
- package/templates/.claudeignore +62 -45
- package/templates/AGENTS.md +1 -0
- package/templates/CLAUDE.md +13 -0
- package/templates/claude-agents/architect.md +4 -2
- package/templates/claude-agents/verifier.md +1 -1
- package/templates/claude-agents/worker.md +7 -4
- package/templates/dot-ai/api_contracts.json +66 -66
- package/templates/dot-ai/context.md +38 -38
- package/templates/dot-ai/db_schema.json +39 -39
- package/dist/commands/session.d.ts +0 -22
- package/dist/commands/session.js +0 -106
- package/dist/core/session-memory.d.ts +0 -42
- package/dist/core/session-memory.js +0 -121
package/dist/core/spec-slices.js
CHANGED
|
@@ -16,9 +16,7 @@ function cap(text, max) {
|
|
|
16
16
|
return { text, truncated: false };
|
|
17
17
|
return { text: text.slice(0, Math.max(0, max)) + TRUNCATION_MARKER, truncated: true };
|
|
18
18
|
}
|
|
19
|
-
function
|
|
20
|
-
const wanted = decodeURIComponent(anchor).toLowerCase();
|
|
21
|
-
const lines = content.split(/\r?\n/);
|
|
19
|
+
function findSection(lines, predicate) {
|
|
22
20
|
const seen = new Map();
|
|
23
21
|
let inFence = false;
|
|
24
22
|
let start = -1;
|
|
@@ -40,17 +38,41 @@ function markdownSection(content, anchor) {
|
|
|
40
38
|
return lines.slice(start, i).join('\n').trimEnd();
|
|
41
39
|
continue;
|
|
42
40
|
}
|
|
43
|
-
const
|
|
41
|
+
const headingText = m[2];
|
|
42
|
+
const base = slugifyHeading(headingText);
|
|
44
43
|
const count = seen.get(base) ?? 0;
|
|
45
44
|
seen.set(base, count + 1);
|
|
46
45
|
const slug = count === 0 ? base : `${base}-${count}`;
|
|
47
|
-
if (slug
|
|
46
|
+
if (predicate(slug, headingText)) {
|
|
48
47
|
start = i;
|
|
49
48
|
level = hLevel;
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
return start >= 0 ? lines.slice(start).join('\n').trimEnd() : null;
|
|
53
52
|
}
|
|
53
|
+
function markdownSection(content, anchor) {
|
|
54
|
+
const wanted = decodeURIComponent(anchor).trim().toLowerCase();
|
|
55
|
+
const lines = content.split(/\r?\n/);
|
|
56
|
+
// 1. Exact slug match (standard markdown anchor)
|
|
57
|
+
const exact = findSection(lines, (slug) => slug === wanted);
|
|
58
|
+
if (exact !== null)
|
|
59
|
+
return exact;
|
|
60
|
+
// 2. Prefix / section number match (e.g. "6" or "15.1" matching "## 6. Coach..." or "### 15.1 Shared Pieces")
|
|
61
|
+
const prefixMatch = findSection(lines, (_slug, headingText) => {
|
|
62
|
+
const raw = headingText.trim().toLowerCase();
|
|
63
|
+
if (raw === wanted)
|
|
64
|
+
return true;
|
|
65
|
+
if (raw.startsWith(`${wanted}.`) || raw.startsWith(`${wanted} `) || raw.startsWith(`${wanted}:`))
|
|
66
|
+
return true;
|
|
67
|
+
const base = slugifyHeading(headingText);
|
|
68
|
+
if (base.startsWith(`${wanted}-`) || base === wanted)
|
|
69
|
+
return true;
|
|
70
|
+
return false;
|
|
71
|
+
});
|
|
72
|
+
if (prefixMatch !== null)
|
|
73
|
+
return prefixMatch;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
54
76
|
function jsonPointerValue(doc, pointer) {
|
|
55
77
|
if (pointer === '')
|
|
56
78
|
return { found: true, value: doc };
|
|
@@ -80,6 +102,73 @@ function jsonPointerValue(doc, pointer) {
|
|
|
80
102
|
}
|
|
81
103
|
return { found: true, value: current };
|
|
82
104
|
}
|
|
105
|
+
function getByDotPath(obj, pathStr) {
|
|
106
|
+
const parts = pathStr.split('.');
|
|
107
|
+
let cur = obj;
|
|
108
|
+
for (const p of parts) {
|
|
109
|
+
if (cur && typeof cur === 'object' && Object.prototype.hasOwnProperty.call(cur, p)) {
|
|
110
|
+
cur = cur[p];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
return { found: false };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { found: true, value: cur };
|
|
117
|
+
}
|
|
118
|
+
function findEntityInDoc(doc, ref) {
|
|
119
|
+
if (!doc || typeof doc !== 'object')
|
|
120
|
+
return { found: false };
|
|
121
|
+
// 1. Direct table match in relevantTables or tables
|
|
122
|
+
const tables = Array.isArray(doc.relevantTables) ? doc.relevantTables : (Array.isArray(doc.tables) ? doc.tables : null);
|
|
123
|
+
if (tables) {
|
|
124
|
+
const table = tables.find((t) => t && (t.name === ref || t.tableName === ref || t.id === ref));
|
|
125
|
+
if (table)
|
|
126
|
+
return { found: true, value: table };
|
|
127
|
+
}
|
|
128
|
+
if (doc.tables && typeof doc.tables === 'object' && !Array.isArray(doc.tables) && Object.prototype.hasOwnProperty.call(doc.tables, ref)) {
|
|
129
|
+
return { found: true, value: doc.tables[ref] };
|
|
130
|
+
}
|
|
131
|
+
// 2. Mail events trigger match
|
|
132
|
+
if (Array.isArray(doc.mailEvents)) {
|
|
133
|
+
const event = doc.mailEvents.find((m) => m && (m.trigger === ref || m.endpoint === ref || m.id === ref));
|
|
134
|
+
if (event)
|
|
135
|
+
return { found: true, value: event };
|
|
136
|
+
}
|
|
137
|
+
// 3. Routes match in routingPolicy or routes
|
|
138
|
+
const routeLists = [];
|
|
139
|
+
if (doc.routingPolicy && typeof doc.routingPolicy === 'object') {
|
|
140
|
+
if (Array.isArray(doc.routingPolicy.playerRoutes))
|
|
141
|
+
routeLists.push(doc.routingPolicy.playerRoutes);
|
|
142
|
+
if (Array.isArray(doc.routingPolicy.venueScopedRoutes))
|
|
143
|
+
routeLists.push(doc.routingPolicy.venueScopedRoutes);
|
|
144
|
+
if (Array.isArray(doc.routingPolicy.adminRoutes))
|
|
145
|
+
routeLists.push(doc.routingPolicy.adminRoutes);
|
|
146
|
+
if (Array.isArray(doc.routingPolicy.routes))
|
|
147
|
+
routeLists.push(doc.routingPolicy.routes);
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(doc.routes))
|
|
150
|
+
routeLists.push(doc.routes);
|
|
151
|
+
for (const list of routeLists) {
|
|
152
|
+
const route = list.find((r) => r && (r.name === ref ||
|
|
153
|
+
r.id === ref ||
|
|
154
|
+
(Array.isArray(r.routeNames) && r.routeNames.includes(ref))));
|
|
155
|
+
if (route)
|
|
156
|
+
return { found: true, value: route };
|
|
157
|
+
}
|
|
158
|
+
// 4. Contract list items (e.g. confirmationPageContracts array)
|
|
159
|
+
for (const key of Object.keys(doc)) {
|
|
160
|
+
const val = doc[key];
|
|
161
|
+
if (Array.isArray(val)) {
|
|
162
|
+
const match = val.find((item) => item && typeof item === 'object' && (item.name === ref ||
|
|
163
|
+
item.id === ref ||
|
|
164
|
+
item.trigger === ref ||
|
|
165
|
+
(Array.isArray(item.routeNames) && item.routeNames.includes(ref))));
|
|
166
|
+
if (match)
|
|
167
|
+
return { found: true, value: match };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { found: false };
|
|
171
|
+
}
|
|
83
172
|
function joinUrl(base, p) {
|
|
84
173
|
return typeof base === 'string' ? base.replace(/\/+$/, '') + p : p;
|
|
85
174
|
}
|
|
@@ -115,9 +204,32 @@ function endpointValue(doc, fragment) {
|
|
|
115
204
|
return byId ? { found: true, value: byId } : { found: false };
|
|
116
205
|
}
|
|
117
206
|
function jsonRefValue(doc, fragment) {
|
|
118
|
-
if (fragment === ''
|
|
207
|
+
if (fragment === '')
|
|
119
208
|
return jsonPointerValue(doc, fragment);
|
|
120
|
-
|
|
209
|
+
// 1. Direct JSON pointer starting with /
|
|
210
|
+
if (fragment.startsWith('/')) {
|
|
211
|
+
const ptr = jsonPointerValue(doc, fragment);
|
|
212
|
+
if (ptr.found)
|
|
213
|
+
return ptr;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// 2. JSON pointer or top-level property without leading slash (e.g. #confirmationPageContracts)
|
|
217
|
+
const ptr = jsonPointerValue(doc, `/${fragment}`);
|
|
218
|
+
if (ptr.found)
|
|
219
|
+
return ptr;
|
|
220
|
+
// 3. Dot-path property navigation (e.g. #contract.section)
|
|
221
|
+
if (fragment.includes('.')) {
|
|
222
|
+
const dot = getByDotPath(doc, fragment);
|
|
223
|
+
if (dot.found)
|
|
224
|
+
return dot;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// 4. Endpoint lookup by method + path or id
|
|
228
|
+
const endpoint = endpointValue(doc, fragment);
|
|
229
|
+
if (endpoint.found)
|
|
230
|
+
return endpoint;
|
|
231
|
+
// 5. Named entity lookup: route names, mail event triggers, DB schema tables
|
|
232
|
+
return findEntityInDoc(doc, fragment);
|
|
121
233
|
}
|
|
122
234
|
/**
|
|
123
235
|
* Resolves specRefs ("ui_specs.md#anchor", "api_contracts.json#/paths/~1x",
|
|
@@ -108,6 +108,8 @@ export declare function hasGeminiApiKey(root?: string): boolean;
|
|
|
108
108
|
* text leaves the machine. `extraSecrets` (e.g. the Gemini key itself) are masked verbatim.
|
|
109
109
|
*/
|
|
110
110
|
export declare function redactSecrets(text: string, extraSecrets?: string[]): string;
|
|
111
|
+
export declare function loadTriageConfig(root: string): Partial<TriageConfig>;
|
|
112
|
+
export declare function saveTriageConfig(root: string, update: Partial<TriageConfig>): void;
|
|
111
113
|
/**
|
|
112
114
|
* Tier 1 AI Strategist Liaison instance managing escalation evaluation,
|
|
113
115
|
* auto-patch application, and configuration.
|
|
@@ -90,6 +90,62 @@ function loadContracts(root) {
|
|
|
90
90
|
uiSpecs: read('ui_specs.md') ?? undefined,
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
+
export function loadTriageConfig(root) {
|
|
94
|
+
const configFile = path.join(root, '.nativ', 'config.json');
|
|
95
|
+
try {
|
|
96
|
+
if (!fs.existsSync(configFile))
|
|
97
|
+
return {};
|
|
98
|
+
const parsed = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
99
|
+
if (parsed && typeof parsed === 'object' && parsed.triage) {
|
|
100
|
+
const res = {};
|
|
101
|
+
if (typeof parsed.triage.autoTriageEnabled === 'boolean') {
|
|
102
|
+
res.autoTriageEnabled = parsed.triage.autoTriageEnabled;
|
|
103
|
+
}
|
|
104
|
+
if (parsed.triage.riskThreshold === 'safe_contracts_only' || parsed.triage.riskThreshold === 'all_non_destructive') {
|
|
105
|
+
res.riskThreshold = parsed.triage.riskThreshold;
|
|
106
|
+
}
|
|
107
|
+
if (typeof parsed.triage.model === 'string' && parsed.triage.model.trim()) {
|
|
108
|
+
res.model = parsed.triage.model.trim();
|
|
109
|
+
}
|
|
110
|
+
return res;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Ignore corrupt or unreadable config file
|
|
115
|
+
}
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
export function saveTriageConfig(root, update) {
|
|
119
|
+
const nativDir = path.join(root, '.nativ');
|
|
120
|
+
const configFile = path.join(nativDir, 'config.json');
|
|
121
|
+
try {
|
|
122
|
+
if (!fs.existsSync(nativDir))
|
|
123
|
+
fs.mkdirSync(nativDir, { recursive: true });
|
|
124
|
+
let existing = {};
|
|
125
|
+
if (fs.existsSync(configFile)) {
|
|
126
|
+
try {
|
|
127
|
+
existing = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
existing = {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const currentTriage = (existing.triage && typeof existing.triage === 'object') ? existing.triage : {};
|
|
134
|
+
if (typeof update.autoTriageEnabled === 'boolean')
|
|
135
|
+
currentTriage.autoTriageEnabled = update.autoTriageEnabled;
|
|
136
|
+
if (update.riskThreshold)
|
|
137
|
+
currentTriage.riskThreshold = update.riskThreshold;
|
|
138
|
+
if (update.model)
|
|
139
|
+
currentTriage.model = update.model;
|
|
140
|
+
existing.triage = currentTriage;
|
|
141
|
+
const tmp = `${configFile}.${process.pid}.${Date.now()}.tmp`;
|
|
142
|
+
fs.writeFileSync(tmp, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
143
|
+
fs.renameSync(tmp, configFile);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Ignore write failures gracefully
|
|
147
|
+
}
|
|
148
|
+
}
|
|
93
149
|
/**
|
|
94
150
|
* Tier 1 AI Strategist Liaison instance managing escalation evaluation,
|
|
95
151
|
* auto-patch application, and configuration.
|
|
@@ -106,11 +162,12 @@ export class Tier1Liaison {
|
|
|
106
162
|
constructor(root, options = {}) {
|
|
107
163
|
this.root = root;
|
|
108
164
|
this.options = options;
|
|
165
|
+
const saved = loadTriageConfig(root);
|
|
109
166
|
this.config = {
|
|
110
167
|
// Off by default: even a fix classified as safe waits for confirmation unless auto-triage is enabled.
|
|
111
|
-
autoTriageEnabled: options.config?.autoTriageEnabled ?? false,
|
|
112
|
-
riskThreshold: options.config?.riskThreshold ?? 'safe_contracts_only',
|
|
113
|
-
model: options.model ?? DEFAULT_MODEL,
|
|
168
|
+
autoTriageEnabled: options.config?.autoTriageEnabled ?? saved.autoTriageEnabled ?? false,
|
|
169
|
+
riskThreshold: options.config?.riskThreshold ?? saved.riskThreshold ?? 'safe_contracts_only',
|
|
170
|
+
model: options.model ?? saved.model ?? DEFAULT_MODEL,
|
|
114
171
|
};
|
|
115
172
|
}
|
|
116
173
|
/** Provider options after the legacy `apiKey` and `model` hints are folded in. */
|
|
@@ -155,6 +212,10 @@ export class Tier1Liaison {
|
|
|
155
212
|
if (body.riskThreshold === 'safe_contracts_only' || body.riskThreshold === 'all_non_destructive') {
|
|
156
213
|
this.config.riskThreshold = body.riskThreshold;
|
|
157
214
|
}
|
|
215
|
+
if (typeof body.model === 'string' && body.model.trim()) {
|
|
216
|
+
this.config.model = body.model.trim();
|
|
217
|
+
}
|
|
218
|
+
saveTriageConfig(this.root, this.config);
|
|
158
219
|
return { ok: true, config: { ...this.config } };
|
|
159
220
|
}
|
|
160
221
|
async evaluate(escalationId) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VerificationResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Execute a verification command in the workspace directory with timeout and output capture.
|
|
4
|
+
*/
|
|
5
|
+
export declare function executeVerification(command: string, cwd: string, timeoutMs?: number): Promise<VerificationResult>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Execute a verification command in the workspace directory with timeout and output capture.
|
|
4
|
+
*/
|
|
5
|
+
export async function executeVerification(command, cwd, timeoutMs = 120_000) {
|
|
6
|
+
const trimmed = (command || '').trim();
|
|
7
|
+
// Treat empty, 'none', or 'skip' as an informational pass
|
|
8
|
+
if (!trimmed || trimmed.toLowerCase() === 'none' || trimmed.toLowerCase() === 'skip') {
|
|
9
|
+
return {
|
|
10
|
+
success: true,
|
|
11
|
+
exitCode: 0,
|
|
12
|
+
command: trimmed || 'none',
|
|
13
|
+
stdout: '',
|
|
14
|
+
stderr: '',
|
|
15
|
+
durationMs: 0,
|
|
16
|
+
skipped: true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Cross-platform compatibility for common test commands on Windows cmd.exe
|
|
20
|
+
let finalCommand = trimmed;
|
|
21
|
+
if (process.platform === 'win32') {
|
|
22
|
+
if (trimmed === 'true')
|
|
23
|
+
finalCommand = 'exit 0';
|
|
24
|
+
else if (trimmed === 'false')
|
|
25
|
+
finalCommand = 'exit 1';
|
|
26
|
+
}
|
|
27
|
+
const start = Date.now();
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
exec(finalCommand, {
|
|
30
|
+
cwd,
|
|
31
|
+
timeout: timeoutMs,
|
|
32
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
33
|
+
env: process.env,
|
|
34
|
+
}, (err, stdout, stderr) => {
|
|
35
|
+
const durationMs = Date.now() - start;
|
|
36
|
+
const out = (stdout || '').toString();
|
|
37
|
+
const errOut = (stderr || '').toString();
|
|
38
|
+
if (err) {
|
|
39
|
+
const exitCode = typeof err.code === 'number' ? err.code : 1;
|
|
40
|
+
resolve({
|
|
41
|
+
success: false,
|
|
42
|
+
exitCode,
|
|
43
|
+
command: trimmed,
|
|
44
|
+
stdout: out,
|
|
45
|
+
stderr: errOut || err.message,
|
|
46
|
+
durationMs,
|
|
47
|
+
error: err.killed ? `Command timed out after ${timeoutMs}ms` : err.message,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
resolve({
|
|
52
|
+
success: true,
|
|
53
|
+
exitCode: 0,
|
|
54
|
+
command: trimmed,
|
|
55
|
+
stdout: out,
|
|
56
|
+
stderr: errOut,
|
|
57
|
+
durationMs,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PhasesOutcome, VerifyPhasesConfig } from './types.js';
|
|
2
|
+
/** `verifyPhases` from the main checkout's `.nativ/config.json`, never from a worktree's own copy. */
|
|
3
|
+
export declare function loadVerifyPhases(configDir: string): VerifyPhasesConfig;
|
|
4
|
+
/** Runs the configured phases in order and stops at the first failure. */
|
|
5
|
+
export declare function runVerifyPhases(cwd: string, configDir: string, timeout?: number): Promise<PhasesOutcome>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseJsonLoose } from '../enforcement.js';
|
|
4
|
+
import { resolveMainRoot } from '../root-resolver.js';
|
|
5
|
+
import { executeVerification } from './executor.js';
|
|
6
|
+
/** Reads the file, retrying once: antivirus or an editor saving can briefly lock it on Windows. */
|
|
7
|
+
function readConfigText(file) {
|
|
8
|
+
for (let attempt = 0;; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
return fs.readFileSync(file, 'utf8');
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (err?.code === 'ENOENT')
|
|
14
|
+
return null;
|
|
15
|
+
if (attempt >= 1)
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** `verifyPhases` from the main checkout's `.nativ/config.json`, never from a worktree's own copy. */
|
|
21
|
+
export function loadVerifyPhases(configDir) {
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = readConfigText(path.join(resolveMainRoot(configDir), '.nativ', 'config.json'));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { phases: [], error: `.nativ/config.json cannot be read (${err?.message})` };
|
|
28
|
+
}
|
|
29
|
+
if (raw === null)
|
|
30
|
+
return { phases: [] };
|
|
31
|
+
if (!raw.trim())
|
|
32
|
+
return { phases: [], error: '.nativ/config.json is empty' };
|
|
33
|
+
let phases;
|
|
34
|
+
try {
|
|
35
|
+
phases = parseJsonLoose(raw)?.verifyPhases;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return { phases: [], error: `.nativ/config.json is not valid JSON (${err?.message})` };
|
|
39
|
+
}
|
|
40
|
+
if (phases === undefined || phases === null)
|
|
41
|
+
return { phases: [] };
|
|
42
|
+
// An empty or null "run" switches a phase off; a missing or mistyped field is a mistake.
|
|
43
|
+
const isEntry = (p) => typeof p?.name === 'string' && p.name.trim()
|
|
44
|
+
&& (p.run === null || typeof p.run === 'string');
|
|
45
|
+
if (!Array.isArray(phases) || !phases.every(isEntry)) {
|
|
46
|
+
return { phases: [], error: 'verifyPhases must be a list of { "name": "...", "run": "..." } entries' };
|
|
47
|
+
}
|
|
48
|
+
const active = phases.filter((p) => p.run && p.run.trim());
|
|
49
|
+
return { phases: active.map((p) => ({ name: p.name.trim(), run: p.run.trim() })) };
|
|
50
|
+
}
|
|
51
|
+
function configFailure(message) {
|
|
52
|
+
const error = `${message}, so the configured checks could not run. Fix it and verify again.`;
|
|
53
|
+
const command = '.nativ/config.json';
|
|
54
|
+
return { success: false, exitCode: 1, command, stdout: '', stderr: error, durationMs: 0, error };
|
|
55
|
+
}
|
|
56
|
+
/** Runs the configured phases in order and stops at the first failure. */
|
|
57
|
+
export async function runVerifyPhases(cwd, configDir, timeout) {
|
|
58
|
+
const config = loadVerifyPhases(configDir);
|
|
59
|
+
if (config.error)
|
|
60
|
+
return { reports: [], failure: configFailure(config.error) };
|
|
61
|
+
const reports = [];
|
|
62
|
+
for (const phase of config.phases) {
|
|
63
|
+
const res = await executeVerification(phase.run, cwd, timeout);
|
|
64
|
+
reports.push({ name: phase.name, command: res.command, success: res.success, durationMs: res.durationMs });
|
|
65
|
+
if (!res.success) {
|
|
66
|
+
const cause = res.error ? `: ${res.error.trim()}` : '';
|
|
67
|
+
return { reports, failure: { ...res, phases: reports, error: `Phase "${phase.name}" failed${cause}` } };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { reports };
|
|
71
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MasterPlan, MasterPlanTask } from '../../scanner/types.js';
|
|
2
|
+
import type { BatchVerificationResult, PhasesOutcome, TaskVerificationResult, VerificationResult } from './types.js';
|
|
3
|
+
export declare function loadMasterPlan(targetDir: string): {
|
|
4
|
+
planPath: string;
|
|
5
|
+
plan: MasterPlan | null;
|
|
6
|
+
error?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Run the code-shape check on a task's target files and print a short summary to stderr.
|
|
10
|
+
* In 'warn' mode (default) the result is untouched; in 'block' mode issues fail it with a reason.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyCodeShape(result: VerificationResult, targetDir: string, task: MasterPlanTask): VerificationResult;
|
|
13
|
+
/**
|
|
14
|
+
* The one verification every path shares: the configured phases in order, stopping at the
|
|
15
|
+
* first failure, then the task's own command, then the code-shape check.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runTaskVerification(task: MasterPlanTask, options: {
|
|
18
|
+
cwd: string;
|
|
19
|
+
configDir?: string;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
phases?: PhasesOutcome;
|
|
22
|
+
}): Promise<VerificationResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Run verification for a single specific task in the plan.
|
|
25
|
+
*/
|
|
26
|
+
export declare function verifyTask(taskId: string, targetDirArg?: string, options?: {
|
|
27
|
+
timeout?: number;
|
|
28
|
+
}): Promise<TaskVerificationResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Verify a batch of tasks (by milestone, all completed tasks, or the active in-progress task).
|
|
31
|
+
*/
|
|
32
|
+
export declare function verifyBatch(targetDirArg?: string, options?: {
|
|
33
|
+
taskId?: string;
|
|
34
|
+
milestoneId?: string;
|
|
35
|
+
all?: boolean;
|
|
36
|
+
timeout?: number;
|
|
37
|
+
}): Promise<BatchVerificationResult>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { checkCodeShape, formatCodeShapeReport } from '../code-shape.js';
|
|
4
|
+
import { executeVerification } from './executor.js';
|
|
5
|
+
import { runVerifyPhases } from './phases.js';
|
|
6
|
+
export function loadMasterPlan(targetDir) {
|
|
7
|
+
const planPath = path.join(targetDir, '.ai', 'master_plan.json');
|
|
8
|
+
if (!fs.existsSync(planPath)) {
|
|
9
|
+
return { planPath, plan: null, error: `No .ai/master_plan.json found at: ${planPath}` };
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const raw = fs.readFileSync(planPath, 'utf8');
|
|
13
|
+
const plan = JSON.parse(raw);
|
|
14
|
+
return { planPath, plan };
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return { planPath, plan: null, error: `Failed to parse .ai/master_plan.json: ${err.message}` };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run the code-shape check on a task's target files and print a short summary to stderr.
|
|
22
|
+
* In 'warn' mode (default) the result is untouched; in 'block' mode issues fail it with a reason.
|
|
23
|
+
*/
|
|
24
|
+
export function applyCodeShape(result, targetDir, task) {
|
|
25
|
+
if (result.skipped || !result.success)
|
|
26
|
+
return result;
|
|
27
|
+
const report = checkCodeShape(targetDir, task.targetFiles || []);
|
|
28
|
+
if (report.mode === 'off')
|
|
29
|
+
return result;
|
|
30
|
+
result.codeShape = report;
|
|
31
|
+
const summary = formatCodeShapeReport(report);
|
|
32
|
+
if (summary)
|
|
33
|
+
console.error(summary);
|
|
34
|
+
if (report.mode === 'block' && report.issues.length > 0) {
|
|
35
|
+
result.success = false;
|
|
36
|
+
result.exitCode = 1;
|
|
37
|
+
result.error = `Code shape check failed: ${report.issues.length} issue(s) (codeStyle.mode is "block")`;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The one verification every path shares: the configured phases in order, stopping at the
|
|
43
|
+
* first failure, then the task's own command, then the code-shape check.
|
|
44
|
+
*/
|
|
45
|
+
export async function runTaskVerification(task, options) {
|
|
46
|
+
const configDir = options.configDir ?? options.cwd;
|
|
47
|
+
const outcome = options.phases ?? (await runVerifyPhases(options.cwd, configDir, options.timeout));
|
|
48
|
+
// In a batch the shared phase time is counted once, by the batch, not again for every task.
|
|
49
|
+
if (outcome.failure) {
|
|
50
|
+
return { ...outcome.failure, phases: outcome.reports, ...(options.phases ? { durationMs: 0 } : {}) };
|
|
51
|
+
}
|
|
52
|
+
const raw = await executeVerification(task.verificationCommand, options.cwd, options.timeout);
|
|
53
|
+
if (outcome.reports.length) {
|
|
54
|
+
raw.phases = outcome.reports;
|
|
55
|
+
raw.durationMs += options.phases ? 0 : outcome.reports.reduce((sum, r) => sum + r.durationMs, 0);
|
|
56
|
+
raw.skipped = false;
|
|
57
|
+
}
|
|
58
|
+
return applyCodeShape(raw, options.cwd, task);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Run verification for a single specific task in the plan.
|
|
62
|
+
*/
|
|
63
|
+
export async function verifyTask(taskId, targetDirArg, options = {}) {
|
|
64
|
+
const targetDir = path.resolve(targetDirArg || process.cwd());
|
|
65
|
+
const { plan, error } = loadMasterPlan(targetDir);
|
|
66
|
+
if (!plan) {
|
|
67
|
+
return {
|
|
68
|
+
taskId,
|
|
69
|
+
title: 'Unknown',
|
|
70
|
+
milestoneId: 'unknown',
|
|
71
|
+
milestoneName: 'Unknown',
|
|
72
|
+
status: 'unknown',
|
|
73
|
+
result: {
|
|
74
|
+
success: false,
|
|
75
|
+
exitCode: 1,
|
|
76
|
+
command: '',
|
|
77
|
+
stdout: '',
|
|
78
|
+
stderr: error || 'Failed to load master plan',
|
|
79
|
+
durationMs: 0,
|
|
80
|
+
error: error || 'Failed to load master plan',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let foundTask = null;
|
|
85
|
+
let foundMilestone = null;
|
|
86
|
+
for (const m of plan.milestones) {
|
|
87
|
+
const t = m.tasks.find((item) => item.id === taskId);
|
|
88
|
+
if (t) {
|
|
89
|
+
foundTask = t;
|
|
90
|
+
foundMilestone = m;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!foundTask || !foundMilestone) {
|
|
95
|
+
return {
|
|
96
|
+
taskId,
|
|
97
|
+
title: 'Not Found',
|
|
98
|
+
milestoneId: 'unknown',
|
|
99
|
+
milestoneName: 'Unknown',
|
|
100
|
+
status: 'unknown',
|
|
101
|
+
result: {
|
|
102
|
+
success: false,
|
|
103
|
+
exitCode: 1,
|
|
104
|
+
command: '',
|
|
105
|
+
stdout: '',
|
|
106
|
+
stderr: `Task [${taskId}] not found in .ai/master_plan.json`,
|
|
107
|
+
durationMs: 0,
|
|
108
|
+
error: `Task [${taskId}] not found`,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const result = await runTaskVerification(foundTask, { cwd: targetDir, timeout: options.timeout });
|
|
113
|
+
return {
|
|
114
|
+
taskId: foundTask.id,
|
|
115
|
+
title: foundTask.title,
|
|
116
|
+
milestoneId: foundMilestone.id,
|
|
117
|
+
milestoneName: foundMilestone.name,
|
|
118
|
+
status: foundTask.status,
|
|
119
|
+
result,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Verify a batch of tasks (by milestone, all completed tasks, or the active in-progress task).
|
|
124
|
+
*/
|
|
125
|
+
export async function verifyBatch(targetDirArg, options = {}) {
|
|
126
|
+
const targetDir = path.resolve(targetDirArg || process.cwd());
|
|
127
|
+
const { plan, error } = loadMasterPlan(targetDir);
|
|
128
|
+
if (!plan) {
|
|
129
|
+
return {
|
|
130
|
+
projectName: 'unknown',
|
|
131
|
+
total: 0,
|
|
132
|
+
passed: 0,
|
|
133
|
+
failed: 1,
|
|
134
|
+
skipped: 0,
|
|
135
|
+
durationMs: 0,
|
|
136
|
+
tasks: [
|
|
137
|
+
{
|
|
138
|
+
taskId: 'init',
|
|
139
|
+
title: 'Plan Loader',
|
|
140
|
+
milestoneId: 'unknown',
|
|
141
|
+
milestoneName: 'Unknown',
|
|
142
|
+
status: 'unknown',
|
|
143
|
+
result: {
|
|
144
|
+
success: false,
|
|
145
|
+
exitCode: 1,
|
|
146
|
+
command: '',
|
|
147
|
+
stdout: '',
|
|
148
|
+
stderr: error || 'Plan not found',
|
|
149
|
+
durationMs: 0,
|
|
150
|
+
error: error || 'Plan not found',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Filter tasks to run
|
|
157
|
+
const candidates = [];
|
|
158
|
+
for (const m of plan.milestones) {
|
|
159
|
+
for (const t of m.tasks) {
|
|
160
|
+
if (options.taskId && t.id === options.taskId) {
|
|
161
|
+
candidates.push({ task: t, milestone: m });
|
|
162
|
+
}
|
|
163
|
+
else if (options.milestoneId && (m.id === options.milestoneId || m.name.toLowerCase() === options.milestoneId.toLowerCase())) {
|
|
164
|
+
candidates.push({ task: t, milestone: m });
|
|
165
|
+
}
|
|
166
|
+
else if (options.all) {
|
|
167
|
+
// All tasks with verification commands (prefer completed or in_progress, but include all)
|
|
168
|
+
candidates.push({ task: t, milestone: m });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// If no filters specified, find the current in_progress task, or next ready task
|
|
173
|
+
if (!options.taskId && !options.milestoneId && !options.all) {
|
|
174
|
+
let activeTask = null;
|
|
175
|
+
for (const m of plan.milestones) {
|
|
176
|
+
const inProgress = m.tasks.find((t) => t.status === 'in_progress');
|
|
177
|
+
if (inProgress) {
|
|
178
|
+
activeTask = { task: inProgress, milestone: m };
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!activeTask) {
|
|
183
|
+
for (const m of plan.milestones) {
|
|
184
|
+
const pending = m.tasks.find((t) => t.status === 'pending');
|
|
185
|
+
if (pending) {
|
|
186
|
+
activeTask = { task: pending, milestone: m };
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (activeTask) {
|
|
192
|
+
candidates.push(activeTask);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const results = [];
|
|
196
|
+
let passed = 0;
|
|
197
|
+
let failed = 0;
|
|
198
|
+
let skipped = 0;
|
|
199
|
+
let totalDuration = 0;
|
|
200
|
+
// The phases check the whole project, so one run serves every task in the batch.
|
|
201
|
+
const phases = candidates.length ? await runVerifyPhases(targetDir, targetDir, options.timeout) : { reports: [] };
|
|
202
|
+
totalDuration += phases.reports.reduce((sum, r) => sum + r.durationMs, 0);
|
|
203
|
+
for (const { task, milestone } of candidates) {
|
|
204
|
+
const vResult = await runTaskVerification(task, { cwd: targetDir, timeout: options.timeout, phases });
|
|
205
|
+
totalDuration += vResult.durationMs;
|
|
206
|
+
if (vResult.skipped) {
|
|
207
|
+
skipped++;
|
|
208
|
+
}
|
|
209
|
+
else if (vResult.success) {
|
|
210
|
+
passed++;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
failed++;
|
|
214
|
+
}
|
|
215
|
+
results.push({
|
|
216
|
+
taskId: task.id,
|
|
217
|
+
title: task.title,
|
|
218
|
+
milestoneId: milestone.id,
|
|
219
|
+
milestoneName: milestone.name,
|
|
220
|
+
status: task.status,
|
|
221
|
+
result: vResult,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
projectName: plan.projectName,
|
|
226
|
+
total: candidates.length,
|
|
227
|
+
passed,
|
|
228
|
+
failed,
|
|
229
|
+
skipped,
|
|
230
|
+
durationMs: totalDuration,
|
|
231
|
+
tasks: results,
|
|
232
|
+
};
|
|
233
|
+
}
|