@williambeto/ai-workflow 2.3.6 → 2.3.7

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/AGENTS.md CHANGED
@@ -25,3 +25,8 @@ Atlas routes. Nexus owns discovery, requirements, and spec authoring. Orion owns
25
25
  ## Evidence
26
26
 
27
27
  Quick and standard work require only branch, changed areas, commands/results, and limitations in the handoff. Full/release/audit/security work may persist machine-generated evidence.
28
+
29
+ ## Core Concept
30
+
31
+ AI Workflow is an npm CLI that installs agents, commands, skills, policies and templates into your repository.
32
+ It guides coding agents from request to implementation, validation and evidence. AI Workflow Kit preserves workflow integrity so coding agents cannot skip from request to code to success without an explicit implementation path, proportional validation, and final evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williambeto/ai-workflow",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "AI Workflow Kit — OpenCode-first software delivery workflow with agents, commands, skills, validation, and evidence",
5
5
  "license": "MIT",
6
6
  "author": "José Willams",
@@ -5,13 +5,13 @@ import { buildDeliverySummary, formatDeliverySummary } from "../core/validation/
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
7
 
8
- export async function runCollectEvidence({ cwd, exitOnError = true, taskSlug = null, mode = null, dryRun = false, profile = "generic", branchRecovery = "NOT_RECORDED" }) {
8
+ export async function runCollectEvidence({ cwd, exitOnError = true, taskSlug = null, mode = null, dryRun = false, profile = "generic", branchRecovery = "NOT_RECORDED", userRequest = null, explicitApprovals = [] }) {
9
9
  const qualityGuard = new QualityGuard({ cwd, taskSlug, mode });
10
10
  const planner = new ValidationPlanner({ cwd, qualityGuard });
11
11
  const tasks = await planner.plan(profile);
12
12
 
13
13
  const persist = !dryRun && mode === "full";
14
- const collector = new EvidenceCollector({ cwd, timeout: 60000, taskSlug, mode, profile, branchRecovery });
14
+ const collector = new EvidenceCollector({ cwd, timeout: 60000, taskSlug, mode, profile, branchRecovery, userRequest, explicitApprovals });
15
15
  const evidence = await collector.collect(tasks, { writeArtifact: persist });
16
16
  const summary = buildDeliverySummary({ evidence });
17
17
 
@@ -257,7 +257,9 @@ export async function runExecute({ cwd, naturalRequest, taskSlug: taskSlugOverri
257
257
  taskSlug,
258
258
  mode: plan.mode,
259
259
  profile: plan.profile,
260
- branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED"
260
+ branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED",
261
+ userRequest: naturalRequest,
262
+ explicitApprovals: process.env.AI_WORKFLOW_APPROVALS ? process.env.AI_WORKFLOW_APPROVALS.split(",").map(a => a.trim()) : []
261
263
  });
262
264
  const quality = await new QualityGuard({ cwd, taskSlug, mode: plan.mode }).verify();
263
265
  return combineValidation(evidence, quality);
@@ -329,7 +331,14 @@ export async function runExecute({ cwd, naturalRequest, taskSlug: taskSlugOverri
329
331
  finalStatus = "FAIL_QUALITY_GATE";
330
332
  } else {
331
333
  // Workspace stabilized, now run Artifact Fidelity Gate
332
- const fidelityResult = await finalizer.verifyFidelity(result.evidence?.changedFiles);
334
+ const fidelityResult = await finalizer.verifyFidelity({
335
+ userRequest: naturalRequest,
336
+ projectContext: result.evidence?.deliveryDecision?.projectContext || null,
337
+ deliveryDecision: result.evidence?.deliveryDecision || null,
338
+ changedFiles: result.evidence?.changedFiles || [],
339
+ evidence: result.evidence || null,
340
+ explicitApprovals: process.env.AI_WORKFLOW_APPROVALS ? process.env.AI_WORKFLOW_APPROVALS.split(",").map(a => a.trim()) : []
341
+ });
333
342
  if (!fidelityResult.passed) {
334
343
  console.log(`\n[FINALIZER BLOCKED] BLOCKED: Artifact fidelity check failed!`);
335
344
  console.log(`Reason: ${fidelityResult.reason}`);
@@ -50,4 +50,49 @@ export class EvidenceLedger {
50
50
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
51
51
  await fs.writeFile(targetPath, JSON.stringify(this.events, null, 2), "utf8");
52
52
  }
53
+
54
+ async verifyRoleConfinement() {
55
+ const violations = [];
56
+ const events = this.events;
57
+
58
+ const atlasImplements = events.some(e => e.actor === "Atlas" && (e.eventType === "implementation_start" || e.eventType === "implementation_complete"));
59
+ if (atlasImplements) {
60
+ violations.push("Atlas cannot implement directly");
61
+ }
62
+
63
+ const nexusMutates = events.some(e => e.actor === "Nexus" && e.eventType === "file_mutate");
64
+ if (nexusMutates) {
65
+ violations.push("Nexus cannot mutate files during discovery");
66
+ }
67
+
68
+ const sageValidates = events.some(e => e.actor === "Sage" && (e.eventType === "validation_start" || e.eventType === "validation_complete"));
69
+ const hasFidelity = events.some(e => e.eventType === "fidelity_check" || e.eventType === "fidelity_verification");
70
+ if (sageValidates && !hasFidelity) {
71
+ violations.push("Sage cannot validate without fidelity evidence");
72
+ }
73
+
74
+ const phoenixRemediates = events.find(e => e.actor === "Phoenix" && e.eventType === "remediation_start");
75
+ if (phoenixRemediates) {
76
+ const findings = phoenixRemediates.data?.findings;
77
+ if (!findings || findings.length === 0) {
78
+ violations.push("Phoenix cannot perform remediation without findings");
79
+ }
80
+ }
81
+
82
+ const orionReleases = events.find(e => e.actor === "Orion" && e.eventType === "release_start");
83
+ if (orionReleases) {
84
+ const approvals = orionReleases.data?.approvals;
85
+ if (!approvals || approvals.length === 0) {
86
+ violations.push("Orion cannot release without explicit approval");
87
+ }
88
+ }
89
+
90
+ const passed = violations.length === 0;
91
+ return {
92
+ passed,
93
+ status: passed ? "PASS" : "BLOCKED",
94
+ reason: violations.length > 0 ? `Role violations: ${violations.join("; ")}` : "Verification passed",
95
+ violations
96
+ };
97
+ }
53
98
  }
@@ -32,16 +32,65 @@ export class Finalizer {
32
32
  }
33
33
  }
34
34
 
35
- async verifyFidelity(changedFiles = null) {
35
+ async verifyFidelity(arg) {
36
+ let userRequest = null;
37
+ let projectContext = null;
38
+ let deliveryDecision = null;
39
+ let changedFiles = null;
40
+ let evidence = null;
41
+ let explicitApprovals = [];
42
+
43
+ if (Array.isArray(arg)) {
44
+ changedFiles = arg;
45
+ } else if (arg && typeof arg === "object") {
46
+ userRequest = arg.userRequest;
47
+ projectContext = arg.projectContext;
48
+ deliveryDecision = arg.deliveryDecision;
49
+ changedFiles = arg.changedFiles;
50
+ evidence = arg.evidence;
51
+ explicitApprovals = arg.explicitApprovals || [];
52
+ }
53
+
36
54
  const filesToCheck = changedFiles || this.getChangedFilesFromGit();
37
55
  const decisionEngine = new DeliveryDecisionEngine({ cwd: this.cwd });
38
- const deliveryClass = await decisionEngine.determineClass(filesToCheck);
39
- const fidelityGate = new ArtifactFidelityGate({ cwd: this.cwd, deliveryClass });
40
- const result = await fidelityGate.verify(filesToCheck);
56
+ const actualProjectContext = projectContext || await decisionEngine.detectContext();
57
+ const actualUserRequest = userRequest || "";
58
+
59
+ const actualDeliveryDecision = deliveryDecision || await decisionEngine.determineDecision({
60
+ userRequest: actualUserRequest,
61
+ projectContext: actualProjectContext,
62
+ explicitApprovals
63
+ });
64
+
65
+ const actualDeliveryClass = await decisionEngine.determineClass(filesToCheck);
66
+
67
+ const fidelityGate = new ArtifactFidelityGate({
68
+ cwd: this.cwd,
69
+ deliveryClass: actualDeliveryClass,
70
+ userRequest: actualUserRequest,
71
+ explicitApprovals
72
+ });
73
+
74
+ const requestFidelity = await fidelityGate.verifyRequestFidelity({
75
+ userRequest: actualUserRequest,
76
+ deliveryDecision: actualDeliveryDecision,
77
+ projectContext: actualProjectContext,
78
+ changedFiles: filesToCheck,
79
+ evidence,
80
+ explicitApprovals
81
+ });
82
+
83
+ const verifyResult = await fidelityGate.verify(filesToCheck);
84
+
85
+ const passed = requestFidelity.passed && verifyResult.passed;
86
+ const reason = !requestFidelity.passed ? requestFidelity.reason : verifyResult.reason;
87
+
41
88
  return {
42
- passed: result.passed,
43
- reason: result.reason,
44
- deliveryClass
89
+ passed,
90
+ status: requestFidelity.status || (passed ? "PASS" : "BLOCKED"),
91
+ reason,
92
+ deliveryClass: actualDeliveryClass,
93
+ requestFidelity
45
94
  };
46
95
  }
47
96
 
@@ -2,9 +2,11 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  export class ArtifactFidelityGate {
5
- constructor({ cwd = process.cwd(), deliveryClass } = {}) {
5
+ constructor({ cwd = process.cwd(), deliveryClass, userRequest, explicitApprovals } = {}) {
6
6
  this.cwd = cwd;
7
7
  this.deliveryClass = deliveryClass;
8
+ this.userRequest = userRequest;
9
+ this.explicitApprovals = explicitApprovals;
8
10
  }
9
11
 
10
12
  async verify(changedFiles = []) {
@@ -16,7 +18,6 @@ export class ArtifactFidelityGate {
16
18
  for (const file of changedFiles) {
17
19
  const ext = path.extname(file).toLowerCase();
18
20
 
19
- // 1. Forbid adding/modifying HTML files that are not SPA entry points
20
21
  if (ext === ".html" || ext === ".htm") {
21
22
  const basename = path.basename(file).toLowerCase();
22
23
  const isSpaEntry = basename === "index.html" &&
@@ -28,7 +29,6 @@ export class ArtifactFidelityGate {
28
29
  };
29
30
  }
30
31
 
31
- // Check if SPA entry imports framework libraries via CDN script tags
32
32
  try {
33
33
  const content = await fs.readFile(path.join(this.cwd, file), "utf8");
34
34
  if (content.includes("unpkg.com") || content.includes("cdnjs.cloudflare.com") || content.includes("cdn.jsdelivr.net")) {
@@ -60,4 +60,279 @@ export class ArtifactFidelityGate {
60
60
 
61
61
  return { passed: true };
62
62
  }
63
+
64
+ async verifyRequestFidelity(arg) {
65
+ let userRequest = this.userRequest || "";
66
+ let deliveryDecision = this.deliveryDecision || {};
67
+ let projectContext = this.projectContext || {};
68
+ let changedFiles = [];
69
+ let explicitApprovals = this.explicitApprovals || [];
70
+
71
+ if (Array.isArray(arg)) {
72
+ changedFiles = arg;
73
+ } else if (arg && typeof arg === "object") {
74
+ userRequest = arg.userRequest || userRequest;
75
+ deliveryDecision = arg.deliveryDecision || deliveryDecision;
76
+ projectContext = arg.projectContext || projectContext;
77
+ changedFiles = arg.changedFiles || changedFiles;
78
+ explicitApprovals = arg.explicitApprovals || explicitApprovals;
79
+ }
80
+
81
+ const blockingDecisions = new Set([
82
+ "BLOCK_STACK_CONFLICT",
83
+ "BLOCK_UNCLEAR_SCOPE",
84
+ "BLOCK_UNSUPPORTED_CONTEXT",
85
+ "REQUIRE_EXPLICIT_AUTHORIZATION",
86
+ "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL"
87
+ ]);
88
+
89
+ if (blockingDecisions.has(deliveryDecision?.decision)) {
90
+ const filtered = changedFiles.filter(file =>
91
+ !file.startsWith(".ai-workflow/") &&
92
+ !file.startsWith(".git/") &&
93
+ file !== "EVIDENCE.json" &&
94
+ file !== "EVIDENCE.temp.json"
95
+ );
96
+ return {
97
+ status: "BLOCKED",
98
+ passed: false,
99
+ reason: `Delivery decision is blocking: ${deliveryDecision.decision}`,
100
+ decision: deliveryDecision.decision,
101
+ checkedFiles: filtered,
102
+ violations: [`Blocking delivery decision: ${deliveryDecision.decision}`]
103
+ };
104
+ }
105
+
106
+ const filteredFiles = changedFiles.filter(file =>
107
+ !file.startsWith(".ai-workflow/") &&
108
+ !file.startsWith(".git/") &&
109
+ file !== "EVIDENCE.json" &&
110
+ file !== "EVIDENCE.temp.json"
111
+ );
112
+
113
+ const req = userRequest.toLowerCase();
114
+ const isReadOnly = /(inspect|audit|read-only|dependencies)/.test(req);
115
+ const isDocsOnly = /(document|doc|readme)/.test(req) && !req.includes("code");
116
+ const isRelease = /(release|publish|deploy)/.test(req);
117
+
118
+ const violations = [];
119
+
120
+ if (isReadOnly && filteredFiles.length > 0) {
121
+ violations.push("Read-only audit task mutated repository files mismatch.");
122
+ }
123
+
124
+ if (isDocsOnly) {
125
+ const codeChanged = filteredFiles.some(file =>
126
+ !file.endsWith(".md") && !file.endsWith(".txt") && !file.startsWith("docs/")
127
+ );
128
+ if (codeChanged) {
129
+ violations.push("Documentation-only task mutated codebase files without justification mismatch.");
130
+ }
131
+ }
132
+
133
+ if (isRelease) {
134
+ const approved = explicitApprovals.includes("npm-publish") || explicitApprovals.includes("deploy-production");
135
+ if (!approved) {
136
+ violations.push("Release/deploy task proceeds without explicit approval token mismatch.");
137
+ }
138
+ }
139
+
140
+ const reqReact = /(react|next)/.test(req);
141
+ const reqVue = /(vue|nuxt)/.test(req);
142
+ const reqBackend = /(laravel|django|rails|express|nest|fastify|flask)/.test(req);
143
+ const reqWp = /(wordpress|wp)/.test(req);
144
+ const reqCli = /(cli|command)/.test(req);
145
+
146
+ if (reqReact && this.deliveryClass === "standalone-web" && !/prototype/.test(req)) {
147
+ violations.push("React framework page requested, but a standalone HTML/CSS substitute mismatch was delivered instead.");
148
+ }
149
+ if (reqVue && this.deliveryClass === "standalone-web" && !/prototype/.test(req)) {
150
+ violations.push("Vue framework page requested, but a standalone HTML/CSS substitute mismatch was delivered instead.");
151
+ }
152
+
153
+ if (reqBackend && filteredFiles.length > 0) {
154
+ const isLaravel = /\blaravel\b/i.test(req);
155
+ const isDjango = /\bdjango\b/i.test(req);
156
+ const isExpressNestFastify = /\b(express|nest|fastify)\b/i.test(req);
157
+ const isRails = /\brails\b/i.test(req);
158
+
159
+ if (isLaravel) {
160
+ const isLaravelArtifact = filteredFiles.some(file =>
161
+ file === "composer.json" ||
162
+ file === "artisan" ||
163
+ file.startsWith("routes/") ||
164
+ file.startsWith("app/Http/Controllers/") ||
165
+ file.startsWith("app/Models/") ||
166
+ file.startsWith("app/Services/") ||
167
+ file.startsWith("tests/Feature/") ||
168
+ file.startsWith("tests/Unit/") ||
169
+ /(^|\/)(routes\/.*\.php|app\/Http\/Controllers\/.*\.php|app\/Models\/.*\.php|app\/Services\/.*\.php|tests\/Feature\/.*\.php|tests\/Unit\/.*\.php|artisan|composer\.json)$/.test(file)
170
+ );
171
+ if (!isLaravelArtifact) {
172
+ violations.push("Laravel endpoint/scaffold requested, but no valid Laravel files/structure mismatch was delivered.");
173
+ }
174
+ } else if (isDjango) {
175
+ const isDjangoArtifact = filteredFiles.some(file =>
176
+ file === "manage.py" ||
177
+ file === "requirements.txt" ||
178
+ file === "pyproject.toml" ||
179
+ file.endsWith("settings.py") ||
180
+ file.endsWith("urls.py") ||
181
+ file.endsWith("views.py") ||
182
+ file.endsWith("models.py") ||
183
+ file.endsWith("tests.py") ||
184
+ /(^|\/)(manage\.py|settings\.py|urls\.py|views\.py|models\.py|tests\.py|requirements\.txt|pyproject\.toml)$/.test(file)
185
+ );
186
+ if (!isDjangoArtifact) {
187
+ violations.push("Django backend requested, but no valid Django files/structure mismatch was delivered.");
188
+ }
189
+ } else if (isExpressNestFastify) {
190
+ const isExpressNestFastifyArtifact = filteredFiles.some(file =>
191
+ file === "package.json" ||
192
+ file.startsWith("src/routes/") ||
193
+ file.startsWith("src/controllers/") ||
194
+ file.startsWith("src/services/") ||
195
+ file.startsWith("tests/") ||
196
+ /(^|\/)(package\.json|src\/routes\/.*|src\/controllers\/.*|src\/services\/.*|tests\/.*|server\.[a-z]+|app\.[a-z]+|main\.[a-z]+)$/.test(file)
197
+ );
198
+ if (!isExpressNestFastifyArtifact) {
199
+ violations.push("Express/Fastify/Nest backend requested, but no valid Node.js server files/structure mismatch was delivered.");
200
+ }
201
+ } else if (isRails) {
202
+ const isRailsArtifact = filteredFiles.some(file =>
203
+ file === "Gemfile" ||
204
+ file === "config/routes.rb" ||
205
+ file.startsWith("app/controllers/") ||
206
+ file.startsWith("app/models/") ||
207
+ file.startsWith("app/services/") ||
208
+ file.startsWith("test/") ||
209
+ file.startsWith("spec/") ||
210
+ /(^|\/)(Gemfile|config\/routes\.rb|app\/controllers\/.*|app\/models\/.*|app\/services\/.*|test\/.*|spec\/.*)$/.test(file)
211
+ );
212
+ if (!isRailsArtifact) {
213
+ violations.push("Ruby on Rails backend requested, but no valid Rails files/structure mismatch was delivered.");
214
+ }
215
+ } else {
216
+ const isGenericBackendArtifact = filteredFiles.some(file =>
217
+ !file.includes("plain_script.py") &&
218
+ (
219
+ file.startsWith("app/") ||
220
+ file.startsWith("routes/") ||
221
+ file.startsWith("config/") ||
222
+ file.startsWith("src/controllers/") ||
223
+ file.startsWith("src/routes/") ||
224
+ file.startsWith("src/services/") ||
225
+ file.startsWith("backend/") ||
226
+ file.startsWith("server/") ||
227
+ file.startsWith("api/") ||
228
+ file.endsWith(".py") ||
229
+ file.endsWith(".php") ||
230
+ file.endsWith(".rb") ||
231
+ file.endsWith(".go")
232
+ )
233
+ );
234
+ if (!isGenericBackendArtifact) {
235
+ violations.push("Backend endpoint requested, but wrong backend artifact structure mismatch was delivered.");
236
+ }
237
+ }
238
+ }
239
+
240
+ if (reqCli && filteredFiles.length > 0) {
241
+ const isCliArtifact = filteredFiles.some(file =>
242
+ file.startsWith("bin/") || file.startsWith("src/commands/")
243
+ );
244
+ if (!isCliArtifact) {
245
+ violations.push("CLI command requested, but a loose script outside the executable bin/commands configuration mismatch was delivered.");
246
+ }
247
+ }
248
+
249
+ if (reqWp && filteredFiles.length > 0) {
250
+ let isWpArtifact = false;
251
+ for (const file of filteredFiles) {
252
+ const basename = path.basename(file);
253
+ const ext = path.extname(file).toLowerCase();
254
+
255
+ if (file.startsWith("wp-content/") || file === "wp-config.php") {
256
+ isWpArtifact = true;
257
+ break;
258
+ }
259
+
260
+ if (basename === "functions.php" ||
261
+ basename === "front-page.php" ||
262
+ basename === "single.php" ||
263
+ basename === "page.php" ||
264
+ basename === "archive.php" ||
265
+ basename === "theme.json" ||
266
+ file.startsWith("template-parts/") ||
267
+ file.includes("/template-parts/")) {
268
+ isWpArtifact = true;
269
+ break;
270
+ }
271
+ if (basename === "style.css") {
272
+ const hasHeader = await hasWpThemeHeader(this.cwd, file);
273
+ if (hasHeader) {
274
+ isWpArtifact = true;
275
+ break;
276
+ }
277
+ }
278
+ if (basename === "index.php") {
279
+ isWpArtifact = true;
280
+ break;
281
+ }
282
+
283
+ if (basename === "uninstall.php" ||
284
+ basename === "composer.json" ||
285
+ file.startsWith("includes/") || file.includes("/includes/") ||
286
+ file.startsWith("admin/") || file.includes("/admin/") ||
287
+ file.startsWith("public/") || file.includes("/public/") ||
288
+ file.startsWith("src/") || file.includes("/src/")) {
289
+ isWpArtifact = true;
290
+ break;
291
+ }
292
+ if (ext === ".php") {
293
+ const hasHeader = await hasWpPluginHeader(this.cwd, file);
294
+ if (hasHeader) {
295
+ isWpArtifact = true;
296
+ break;
297
+ }
298
+ }
299
+ }
300
+
301
+ const hasHtml = filteredFiles.some(file => path.extname(file).toLowerCase() === ".html" || path.extname(file).toLowerCase() === ".htm");
302
+
303
+ if (!isWpArtifact || hasHtml) {
304
+ violations.push("WordPress plugin or theme requested, but files were delivered outside the WordPress structure or contained standalone HTML.");
305
+ }
306
+ }
307
+
308
+ const status = violations.length > 0 ? "BLOCKED" : "PASS";
309
+ return {
310
+ status,
311
+ passed: status === "PASS",
312
+ reason: violations.length > 0 ? `Fidelity violations: ${violations.join("; ")}` : "Verification passed",
313
+ decision: deliveryDecision.decision || this.deliveryClass,
314
+ checkedFiles: filteredFiles,
315
+ violations
316
+ };
317
+ }
318
+ }
319
+
320
+ async function hasWpThemeHeader(cwd, file) {
321
+ try {
322
+ const fullPath = path.join(cwd, file);
323
+ const content = await fs.readFile(fullPath, "utf8");
324
+ return /\bTheme Name\s*:/i.test(content) || /\bTheme URI\s*:/i.test(content);
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ async function hasWpPluginHeader(cwd, file) {
331
+ try {
332
+ const fullPath = path.join(cwd, file);
333
+ const content = await fs.readFile(fullPath, "utf8");
334
+ return /\bPlugin Name\s*:/i.test(content) || /\bDescription\s*:/i.test(content);
335
+ } catch {
336
+ return false;
337
+ }
63
338
  }
@@ -2,6 +2,29 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { StackDetector } from "./stack-detector.js";
4
4
 
5
+ function parseRequest(request = "") {
6
+ const req = request.toLowerCase();
7
+
8
+ let targetStack = null;
9
+ if (/\b(vue|nuxt|quasar)\b/.test(req)) targetStack = "vue";
10
+ else if (/\b(react|next)\b/.test(req)) targetStack = "react";
11
+ else if (/\b(angular|svelte)\b/.test(req)) targetStack = "svelte-or-angular";
12
+ else if (/\b(laravel|django|rails|express|nest|fastify)\b/.test(req)) {
13
+ if (/\blaravel\b/.test(req)) targetStack = "laravel";
14
+ else targetStack = "backend";
15
+ }
16
+ else if (/\b(wordpress|wp)\b/.test(req)) targetStack = "wordpress";
17
+ else if (/\b(cli|command|bin)\b/.test(req)) targetStack = "cli";
18
+
19
+ let intent = "implementation";
20
+ if (/\b(document|doc|readme)\b/.test(req)) intent = "docs";
21
+ else if (/\b(audit|security|inspect|dependencies)\b/.test(req)) intent = "audit";
22
+ else if (/\b(release|publish|deploy)\b/.test(req)) intent = "release";
23
+ else if (/\b(prototype|standalone html)\b/.test(req)) intent = "prototype";
24
+
25
+ return { targetStack, intent };
26
+ }
27
+
5
28
  export class DeliveryDecisionEngine {
6
29
  constructor({ cwd = process.cwd() } = {}) {
7
30
  this.cwd = cwd;
@@ -13,7 +36,6 @@ export class DeliveryDecisionEngine {
13
36
  return "empty";
14
37
  }
15
38
 
16
- // 1. Docs check
17
39
  const isDocs = changedFiles.every(file =>
18
40
  file.endsWith(".md") || file.endsWith(".txt") || file.startsWith("docs/")
19
41
  );
@@ -21,10 +43,8 @@ export class DeliveryDecisionEngine {
21
43
  return "docs-only";
22
44
  }
23
45
 
24
- // 2. Detect project stacks
25
46
  const stacks = await this.stackDetector.detect();
26
47
 
27
- // Check package.json for frontend frameworks
28
48
  let usesJsFramework = false;
29
49
  try {
30
50
  const pkgPath = path.join(this.cwd, "package.json");
@@ -35,14 +55,11 @@ export class DeliveryDecisionEngine {
35
55
  const frameworks = ["react", "next", "vue", "nuxt", "svelte", "solid-js", "preact", "quasar", "@angular/core"];
36
56
  usesJsFramework = frameworks.some(fw => deps[fw] !== undefined);
37
57
  } catch {
38
- // package.json doesn't exist or is invalid
58
+ // ignore
39
59
  }
40
60
 
41
- // Check for WordPress
42
- const isWordPress = await fs.access(path.join(this.cwd, "wp-config.php")).then(() => true).catch(() => false) ||
43
- await fs.access(path.join(this.cwd, "wp-content")).then(() => true).catch(() => false);
61
+ const isWordPress = await checkWordPressDirectRoot(this.cwd);
44
62
 
45
- // Analyze changed files for UI/frontend assets
46
63
  const hasFrontendFiles = changedFiles.some(file =>
47
64
  /\.(html?|css|scss|sass|less|jsx|tsx|vue|svelte)$/i.test(file) ||
48
65
  (/\.js$/i.test(file) && !file.includes("config") && !file.includes("test"))
@@ -66,4 +83,262 @@ export class DeliveryDecisionEngine {
66
83
 
67
84
  return "generic-code";
68
85
  }
86
+
87
+ async detectContext() {
88
+ const stacks = await this.stackDetector.detect();
89
+
90
+ let usesJsFramework = null;
91
+ let hasPackageJson = false;
92
+ let isCli = false;
93
+ try {
94
+ const pkgPath = path.join(this.cwd, "package.json");
95
+ const pkgContent = await fs.readFile(pkgPath, "utf8");
96
+ hasPackageJson = true;
97
+ const pkg = JSON.parse(pkgContent);
98
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
99
+ if (pkg.bin || pkg.name === "@williambeto/ai-workflow") {
100
+ isCli = true;
101
+ }
102
+
103
+ const frameworks = ["react", "next", "vue", "nuxt", "svelte", "solid-js", "preact", "quasar", "@angular/core"];
104
+ for (const fw of frameworks) {
105
+ if (deps[fw] !== undefined) {
106
+ usesJsFramework = fw;
107
+ break;
108
+ }
109
+ }
110
+ } catch {}
111
+
112
+ let isLaravel = false;
113
+ let hasComposerJson = false;
114
+ try {
115
+ const composerPath = path.join(this.cwd, "composer.json");
116
+ const composerContent = await fs.readFile(composerPath, "utf8");
117
+ hasComposerJson = true;
118
+ const composer = JSON.parse(composerContent);
119
+ const reqs = { ...(composer.require || {}), ...(composer.requireDev || {}) };
120
+ if (reqs["laravel/framework"]) {
121
+ isLaravel = true;
122
+ }
123
+ } catch {}
124
+
125
+ const isWordPress = await checkWordPressDirectRoot(this.cwd);
126
+
127
+ let isEmpty = true;
128
+ try {
129
+ const entries = await fs.readdir(this.cwd);
130
+ const meaningful = entries.filter(e => !e.startsWith(".") && !["node_modules", "EVIDENCE.json"].includes(e));
131
+ if (meaningful.length > 0) {
132
+ if (meaningful.length === 1 && meaningful[0] === "package.json") {
133
+ try {
134
+ const content = await fs.readFile(path.join(this.cwd, "package.json"), "utf8");
135
+ const pkg = JSON.parse(content);
136
+ if (!pkg.dependencies && !pkg.devDependencies) {
137
+ isEmpty = true;
138
+ } else {
139
+ isEmpty = false;
140
+ }
141
+ } catch {
142
+ isEmpty = true;
143
+ }
144
+ } else {
145
+ isEmpty = false;
146
+ }
147
+ }
148
+ } catch {
149
+ isEmpty = true;
150
+ }
151
+
152
+ let projectState = isEmpty ? "empty" : "active";
153
+ let detectedStackType = "unknown";
154
+ let detectedStacks = [];
155
+
156
+ if (isWordPress) {
157
+ detectedStackType = "WordPress context";
158
+ detectedStacks.push("wordpress");
159
+ } else if (isLaravel) {
160
+ detectedStackType = "backend context";
161
+ detectedStacks.push("laravel");
162
+ } else if (usesJsFramework) {
163
+ detectedStackType = "existing matching stack";
164
+ detectedStacks.push(usesJsFramework);
165
+ } else if (isCli) {
166
+ detectedStackType = "CLI package context";
167
+ detectedStacks.push("node");
168
+ } else if (stacks.includes("node")) {
169
+ detectedStackType = "existing matching stack";
170
+ detectedStacks.push("node");
171
+ } else if (stacks.includes("python") || stacks.includes("php")) {
172
+ detectedStackType = "backend context";
173
+ detectedStacks.push(...stacks);
174
+ } else if (projectState === "empty") {
175
+ detectedStackType = "empty project";
176
+ }
177
+
178
+ return {
179
+ projectState,
180
+ detectedStackType,
181
+ detectedStacks,
182
+ isWordPress,
183
+ isLaravel,
184
+ usesJsFramework,
185
+ isCli
186
+ };
187
+ }
188
+
189
+ async determineDecision(input = {}) {
190
+ const userRequest = input.userRequest || "";
191
+ const explicitApprovals = input.explicitApprovals || [];
192
+
193
+ const ctx = input.projectContext || await this.detectContext();
194
+ const projectState = ctx.projectState || "active";
195
+ const detectedStacks = ctx.detectedStacks || [];
196
+ const isWordPress = ctx.isWordPress || false;
197
+ const isLaravel = ctx.isLaravel || false;
198
+ const isCli = ctx.isCli || false;
199
+ const usesJsFramework = ctx.usesJsFramework || null;
200
+
201
+ const parsed = parseRequest(userRequest);
202
+ const targetStack = parsed.targetStack;
203
+ const intent = parsed.intent;
204
+
205
+ let decision = "IMPLEMENT_IN_EXISTING_CONTEXT";
206
+ let allowedArtifactShape = "generic-code";
207
+ let forbiddenSubstitutes = ["success-without-validation"];
208
+ let validationRequired = ["build-or-equivalent"];
209
+ let approvalRequired = false;
210
+
211
+ if (intent === "audit") {
212
+ decision = "READ_ONLY_AUDIT";
213
+ allowedArtifactShape = "audit-report";
214
+ forbiddenSubstitutes.push("any-code-mutation");
215
+ validationRequired = [];
216
+ }
217
+ else if (intent === "docs") {
218
+ decision = "DOCS_ONLY";
219
+ allowedArtifactShape = "markdown";
220
+ forbiddenSubstitutes.push("unjustified-code-mutation");
221
+ validationRequired = [];
222
+ }
223
+ else if (intent === "release") {
224
+ decision = "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL";
225
+ allowedArtifactShape = "package-tarball";
226
+ forbiddenSubstitutes.push("publish-without-approval");
227
+ validationRequired = ["release-readiness-or-equivalent"];
228
+
229
+ const hasApproval = explicitApprovals.includes("npm-publish") || explicitApprovals.includes("deploy-production");
230
+ if (hasApproval) {
231
+ decision = "RELEASE_OR_DEPLOY_APPROVED";
232
+ approvalRequired = true;
233
+ }
234
+ }
235
+ else if (intent === "prototype") {
236
+ decision = "CREATE_STANDALONE_PROTOTYPE";
237
+ allowedArtifactShape = "standalone-html";
238
+ validationRequired = [];
239
+ }
240
+ else if (projectState === "empty") {
241
+ decision = "SCAFFOLD_AND_IMPLEMENT";
242
+ if (targetStack === "vue" || targetStack === "react") {
243
+ allowedArtifactShape = "runnable-framework-app";
244
+ forbiddenSubstitutes.push("standalone-html-as-framework-delivery", "loose-framework-files-without-runtime");
245
+ validationRequired.push("dependency-install-or-verification", "runtime-smoke-or-equivalent");
246
+ }
247
+ } else {
248
+ const hasConflict = (targetStack === "vue" && detectedStacks.includes("react")) ||
249
+ (targetStack === "react" && detectedStacks.includes("vue")) ||
250
+ (targetStack === "laravel" && !detectedStacks.includes("laravel") && detectedStacks.length > 0) ||
251
+ (targetStack === "wordpress" && !detectedStacks.includes("wordpress") && detectedStacks.length > 0);
252
+
253
+ if (hasConflict) {
254
+ decision = "BLOCK_STACK_CONFLICT";
255
+ } else if (targetStack === "laravel" && !detectedStacks.includes("laravel")) {
256
+ decision = "BLOCK_UNSUPPORTED_CONTEXT";
257
+ } else if (targetStack === "wordpress" && !detectedStacks.includes("wordpress")) {
258
+ decision = "BLOCK_UNSUPPORTED_CONTEXT";
259
+ } else {
260
+ if (targetStack === "vue" || targetStack === "react") {
261
+ decision = "IMPLEMENT_IN_EXISTING_CONTEXT";
262
+ allowedArtifactShape = "runnable-framework-app";
263
+ forbiddenSubstitutes.push("standalone-html-as-framework-delivery", "loose-framework-files-without-runtime");
264
+ validationRequired.push("dependency-install-or-verification", "runtime-smoke-or-equivalent");
265
+ } else if (targetStack === "cli" || isCli) {
266
+ decision = "CLI_COMMAND_INTEGRATION";
267
+ allowedArtifactShape = "bin-entry";
268
+ }
269
+ }
270
+ }
271
+
272
+ if (userRequest && userRequest.trim().length < 3) {
273
+ decision = "BLOCK_UNCLEAR_SCOPE";
274
+ }
275
+
276
+ return {
277
+ intent: intent === "implementation" ? "implementation" : intent,
278
+ requestedOutcome: targetStack === "vue" || targetStack === "react" ? "page" : (targetStack === "wordpress" ? "plugin-or-theme" : null),
279
+ requestedStack: targetStack,
280
+ requestedPlatform: targetStack === "wordpress" ? "wordpress" : null,
281
+ projectState,
282
+ detectedStacks,
283
+ decision,
284
+ allowedArtifactShape,
285
+ forbiddenSubstitutes,
286
+ validationRequired,
287
+ approvalRequired: explicitApprovals.length > 0
288
+ };
289
+ }
290
+ }
291
+
292
+ async function checkWordPressDirectRoot(cwd) {
293
+ // Check standard paths
294
+ const hasWpConfig = await fs.access(path.join(cwd, "wp-config.php")).then(() => true).catch(() => false);
295
+ const hasWpContent = await fs.access(path.join(cwd, "wp-content")).then(() => true).catch(() => false);
296
+ if (hasWpConfig || hasWpContent) {
297
+ return true;
298
+ }
299
+
300
+ // Check direct Theme root
301
+ const hasStyleCss = await fs.access(path.join(cwd, "style.css")).then(() => true).catch(() => false);
302
+ if (hasStyleCss) {
303
+ try {
304
+ const content = await fs.readFile(path.join(cwd, "style.css"), "utf8");
305
+ if (/\bTheme Name\s*:/i.test(content) || /\bTheme URI\s*:/i.test(content)) {
306
+ const otherIndicators = [
307
+ "functions.php",
308
+ "theme.json",
309
+ "index.php",
310
+ "front-page.php",
311
+ "single.php",
312
+ "page.php",
313
+ "archive.php",
314
+ "template-parts"
315
+ ];
316
+ for (const ind of otherIndicators) {
317
+ const exists = await fs.access(path.join(cwd, ind)).then(() => true).catch(() => false);
318
+ if (exists) {
319
+ return true;
320
+ }
321
+ }
322
+ }
323
+ } catch {
324
+ // ignore
325
+ }
326
+ }
327
+
328
+ // Check direct Plugin root
329
+ try {
330
+ const files = await fs.readdir(cwd);
331
+ for (const file of files) {
332
+ if (file.endsWith(".php")) {
333
+ const content = await fs.readFile(path.join(cwd, file), "utf8");
334
+ if (/\bPlugin Name\s*:/i.test(content)) {
335
+ return true;
336
+ }
337
+ }
338
+ }
339
+ } catch {
340
+ // ignore
341
+ }
342
+
343
+ return false;
69
344
  }
@@ -2,6 +2,8 @@ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { QualityGuard } from "./quality-guard.js";
5
+ import { DeliveryDecisionEngine } from "./delivery-decision-engine.js";
6
+ import { ArtifactFidelityGate } from "./artifact-fidelity-gate.js";
5
7
 
6
8
  const VALIDATION_KINDS = new Set(["test", "build", "typecheck", "lint", "security", "smoke", "validate", "other"]);
7
9
 
@@ -27,7 +29,7 @@ function inferTaskKind(task = {}) {
27
29
  }
28
30
 
29
31
  export class EvidenceCollector {
30
- constructor({ cwd, maxLogLength = 2000, timeout = 60000, taskSlug = null, mode = null, profile = "generic", branchRecovery = "NOT_RECORDED" } = {}) {
32
+ constructor({ cwd, maxLogLength = 2000, timeout = 60000, taskSlug = null, mode = null, profile = "generic", branchRecovery = "NOT_RECORDED", userRequest = null, explicitApprovals = [] } = {}) {
31
33
  this.cwd = cwd;
32
34
  this.maxLogLength = maxLogLength;
33
35
  this.timeout = timeout;
@@ -35,6 +37,53 @@ export class EvidenceCollector {
35
37
  this.mode = mode;
36
38
  this.profile = profile;
37
39
  this.branchRecovery = branchRecovery;
40
+ this.userRequest = userRequest;
41
+ this.explicitApprovals = explicitApprovals;
42
+ }
43
+
44
+ async findUserRequest() {
45
+ if (this.userRequest) return this.userRequest;
46
+
47
+ if (this.taskSlug) {
48
+ const ledgerPath = path.join(this.cwd, `.ai-workflow/history/${this.taskSlug}-ledger.json`);
49
+ try {
50
+ const content = await fs.readFile(ledgerPath, "utf8");
51
+ const events = JSON.parse(content);
52
+ const routeEvent = events.find(e => e.eventType === "routing");
53
+ if (routeEvent && routeEvent.data?.request) {
54
+ return routeEvent.data.request;
55
+ }
56
+ const startEvent = events.find(e => e.eventType === "implementation_start");
57
+ if (startEvent && startEvent.data?.prompt) {
58
+ return startEvent.data.prompt;
59
+ }
60
+ } catch {}
61
+ }
62
+
63
+ try {
64
+ const dir = path.join(this.cwd, ".ai-workflow/history");
65
+ const files = await fs.readdir(dir);
66
+ for (const file of files) {
67
+ if (file.endsWith("-ledger.json")) {
68
+ const content = await fs.readFile(path.join(dir, file), "utf8");
69
+ const events = JSON.parse(content);
70
+ const routeEvent = events.find(e => e.eventType === "routing");
71
+ if (routeEvent && routeEvent.data?.request) {
72
+ return routeEvent.data.request;
73
+ }
74
+ }
75
+ }
76
+ } catch {}
77
+
78
+ try {
79
+ const { execSync } = await import("node:child_process");
80
+ const branch = execSync("git branch --show-current", { cwd: this.cwd, encoding: "utf8" }).trim();
81
+ if (branch && branch !== "main" && branch !== "master" && branch.startsWith("feat/")) {
82
+ return branch.replace("feat/", "").replaceAll("-", " ");
83
+ }
84
+ } catch {}
85
+
86
+ return "";
38
87
  }
39
88
 
40
89
  runTask(task) {
@@ -95,6 +144,30 @@ export class EvidenceCollector {
95
144
  const behaviorTests = results.filter((result) => result.kind === "test");
96
145
  const passingBehaviorTest = behaviorTests.some((result) => result.status === "PASS");
97
146
 
147
+ const userRequest = await this.findUserRequest();
148
+ const engine = new DeliveryDecisionEngine({ cwd: this.cwd });
149
+ const projectContext = await engine.detectContext();
150
+ const deliveryDecision = await engine.determineDecision({
151
+ userRequest,
152
+ projectContext,
153
+ explicitApprovals: this.explicitApprovals || []
154
+ });
155
+
156
+ const fidelityGate = new ArtifactFidelityGate({
157
+ cwd: this.cwd,
158
+ deliveryClass: deliveryDecision.allowedArtifactShape === "runnable-framework-app" ? "framework-native-frontend" : (deliveryDecision.allowedArtifactShape === "wordpress" ? "wordpress-php" : "generic-code"),
159
+ userRequest,
160
+ explicitApprovals: this.explicitApprovals || []
161
+ });
162
+
163
+ const changedFiles = policyValidation.git?.changedFiles || [];
164
+ const artifactFidelityCheck = await fidelityGate.verifyRequestFidelity({
165
+ userRequest,
166
+ deliveryDecision,
167
+ projectContext,
168
+ changedFiles
169
+ });
170
+
98
171
  let overallStatus = "PASS";
99
172
  if (implementation && executableBehavior && tasks.length === 0) overallStatus = "BLOCKED";
100
173
  else if (results.some((result) => ["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status))) overallStatus = "FAIL_QUALITY_GATE";
@@ -102,6 +175,16 @@ export class EvidenceCollector {
102
175
  else if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(policyValidation.overallStatus)) overallStatus = "FAIL_QUALITY_GATE";
103
176
  else if (results.some((result) => result.status === "PASS_WITH_NOTES") || policyValidation.overallStatus === "PASS_WITH_NOTES") overallStatus = "PASS_WITH_NOTES";
104
177
 
178
+ if (!artifactFidelityCheck || !artifactFidelityCheck.passed) {
179
+ overallStatus = "BLOCKED";
180
+ } else if (implementation) {
181
+ if (!deliveryDecision || !deliveryDecision.decision || deliveryDecision.decision.startsWith("BLOCK") || deliveryDecision.decision.includes("BLOCKED") || deliveryDecision.decision === "REQUIRE_EXPLICIT_AUTHORIZATION" || deliveryDecision.decision === "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL") {
182
+ overallStatus = "BLOCKED";
183
+ } else if (results.length === 0 && executableBehavior && deliveryDecision.decision !== "DOCUMENTATION_ONLY" && deliveryDecision.decision !== "DOCS_ONLY" && deliveryDecision.decision !== "READ_ONLY_AUDIT") {
184
+ overallStatus = "BLOCKED";
185
+ }
186
+ }
187
+
105
188
  const limitations = [];
106
189
  if (implementation && executableBehavior && tasks.length === 0) limitations.push("No meaningful validation command was available for executable implementation work.");
107
190
  if (executableBehavior && behaviorTests.length === 0) {
@@ -118,12 +201,14 @@ export class EvidenceCollector {
118
201
  workflowProfile: this.profile,
119
202
  branchRecovery: this.branchRecovery,
120
203
  branch: policyValidation.git?.branch || "unknown",
121
- changedFiles: policyValidation.git?.changedFiles || [],
204
+ changedFiles,
122
205
  status: publicStatus(overallStatus),
123
206
  internalStatus: overallStatus,
124
207
  commands: results,
125
208
  checks: policyValidation.checks,
126
- limitations
209
+ limitations,
210
+ deliveryDecision,
211
+ artifactFidelityCheck
127
212
  };
128
213
 
129
214
  if (writeArtifact) {