@williambeto/ai-workflow 2.8.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,9 @@ import {
2
2
  ArtifactFidelityGate,
3
3
  DeliveryDecisionEngine,
4
4
  MergeGate,
5
+ RequestRouter,
5
6
  StackDetector
6
- } from "./chunk-XW747GIG.js";
7
+ } from "./chunk-H7GIKXFO.js";
7
8
 
8
9
  // src/core/validation/quality-guard.ts
9
10
  import { execSync } from "child_process";
@@ -39,19 +40,22 @@ var QualityGuard = class {
39
40
  evidencePolicy;
40
41
  riskLevel;
41
42
  _changedFiles;
43
+ scopedChangedFiles;
42
44
  _hasHead;
43
45
  constructor({
44
46
  cwd,
45
47
  taskSlug = null,
46
48
  evidenceWillBePersisted = false,
47
49
  evidencePolicy = "optional",
48
- riskLevel = "medium"
50
+ riskLevel = "medium",
51
+ scopedChangedFiles = null
49
52
  }) {
50
53
  this.cwd = cwd;
51
54
  this.taskSlug = taskSlug;
52
55
  this.evidencePolicy = evidencePolicy || "optional";
53
56
  this.riskLevel = riskLevel || "medium";
54
57
  this._changedFiles = null;
58
+ this.scopedChangedFiles = scopedChangedFiles ? [...new Set(scopedChangedFiles.map(normalizePath).filter(Boolean))].sort() : null;
55
59
  this._hasHead = null;
56
60
  this.evidenceWillBePersisted = evidenceWillBePersisted;
57
61
  }
@@ -92,24 +96,27 @@ var QualityGuard = class {
92
96
  this._changedFiles = [...files].sort();
93
97
  return this._changedFiles;
94
98
  }
99
+ getScopedChangedFiles() {
100
+ return this.scopedChangedFiles ?? this.getChangedFiles();
101
+ }
95
102
  isDocsTask() {
96
- const files = this.getChangedFiles();
103
+ const files = this.getScopedChangedFiles();
97
104
  return files.length > 0 && files.every((file) => file.endsWith(".md") || file.endsWith(".txt") || file.startsWith("docs/"));
98
105
  }
99
106
  isUiTask() {
100
- return this.getChangedFiles().some(
107
+ return this.getScopedChangedFiles().some(
101
108
  (file) => /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file) || /(^|\/)(components|pages|views)\//.test(file)
102
109
  );
103
110
  }
104
111
  isImplementationTask() {
105
- const files = this.getChangedFiles();
112
+ const files = this.getScopedChangedFiles();
106
113
  if (files.length === 0 || this.isDocsTask()) return false;
107
114
  return files.some(
108
115
  (file) => file === "package.json" || /^(src|app|pages|components|server|api)\//.test(file) || /\.(js|jsx|ts|tsx|vue|php|py|rb|go|java|css|scss|html)$/.test(file)
109
116
  );
110
117
  }
111
- async hasExecutableBehaviorChanges() {
112
- const files = this.getChangedFiles().filter((file) => !isManagedChangePath(file));
118
+ async hasExecutableBehaviorChanges(filesOverride) {
119
+ const files = (filesOverride || this.getScopedChangedFiles()).filter((file) => !isManagedChangePath(file));
113
120
  const executableExtensions = /\.(js|jsx|mjs|cjs|ts|tsx|vue|php|py|rb|go|java|cs|rs|swift|kt|kts|sh|bash)$/i;
114
121
  if (files.some((file) => executableExtensions.test(file))) return true;
115
122
  for (const file of files.filter((item) => /\.(html?|xhtml)$/i.test(item))) {
@@ -141,12 +148,15 @@ var QualityGuard = class {
141
148
  }
142
149
  async verifyDocumentationNeed() {
143
150
  if (!this.isImplementationTask()) return { status: "NOT_RUN", reason: "No implementation changes detected." };
144
- const files = this.getChangedFiles();
151
+ const files = this.getScopedChangedFiles();
145
152
  const docsChanged = files.some((file) => file === "README.md" || file.startsWith("docs/") || file.endsWith(".md"));
146
153
  const publicSurfaceChanged = files.some(
147
154
  (file) => file === "package.json" || /(^|\/)(routes?|api|cli|commands?|config|public)\//.test(file) || /README|CHANGELOG/.test(file)
148
155
  );
149
156
  if (publicSurfaceChanged && !docsChanged) {
157
+ if (this.evidencePolicy === "required") {
158
+ return { status: "FAIL_QUALITY_GATE", reason: "A required-evidence change modified a public API, CLI, configuration, or package surface without updating documentation." };
159
+ }
150
160
  return { status: "PASS_WITH_NOTES", reason: "Public behavior may have changed without a documentation update." };
151
161
  }
152
162
  return { status: "PASS" };
@@ -163,7 +173,11 @@ var QualityGuard = class {
163
173
  if (/\.(png|jpe?g|webp)$/i.test(String(entry))) found.push(normalizePath(path.join(dir, String(entry))));
164
174
  }
165
175
  }
166
- return found.length ? { status: "PASS", screenshots: found.slice(0, 20) } : { status: "PASS_WITH_NOTES", reason: "UI changed without persisted screenshots; rely on the stated browser/manual review only if it actually occurred." };
176
+ if (found.length) return { status: "PASS", screenshots: found.slice(0, 20) };
177
+ if (this.evidencePolicy === "required") {
178
+ return { status: "FAIL_QUALITY_GATE", reason: "Required evidence for a high-risk or release UI change does not include a persisted screenshot." };
179
+ }
180
+ return { status: "PASS_WITH_NOTES", reason: "Screenshots are optional for this low/medium-risk UI change and were not persisted." };
167
181
  }
168
182
  async verifyPersistedEvidence() {
169
183
  if (this.evidencePolicy !== "required") return { status: "NOT_RUN", reason: "Persisted evidence is optional under optional evidence policy." };
@@ -373,13 +387,87 @@ var VisualVerifier = class {
373
387
  // src/core/validation/evidence-collector.ts
374
388
  import { spawnSync } from "child_process";
375
389
  import fs3 from "fs/promises";
390
+ import fsSync from "fs";
376
391
  import path3 from "path";
392
+
393
+ // src/core/completion-contract.ts
394
+ var PASSING_VALIDATION = /* @__PURE__ */ new Set(["PASS", "PASS_WITH_NOTES"]);
395
+ var TEST_FILE_PATTERN = /(^|\/)(__tests__|tests?)(\/|\.)|\.(test|spec)\.[cm]?[jt]sx?$/i;
396
+ function parseDeliveryOutcomeClaim(output = "") {
397
+ const matches = [...String(output).matchAll(/^AIWK_DELIVERY_OUTCOME:\s*(CHANGED|ALREADY_SATISFIED)\s*$/gim)];
398
+ if (matches.length !== 1) return null;
399
+ return matches[0][1].toUpperCase();
400
+ }
401
+ function resolveDeliveryOutcome({
402
+ deliveryMode,
403
+ mutationIntent,
404
+ changedFiles = [],
405
+ validationStatus,
406
+ validationObserved = false,
407
+ deliveryDecision = null,
408
+ claimedOutcome = null
409
+ }) {
410
+ if (deliveryDecision?.startsWith("BLOCK_")) {
411
+ return "NEEDS_CLARIFICATION";
412
+ }
413
+ if (!PASSING_VALIDATION.has(validationStatus)) {
414
+ return "NOT_DELIVERED";
415
+ }
416
+ if (mutationIntent === "readonly") {
417
+ return "READ_ONLY_RESULT";
418
+ }
419
+ if (deliveryMode === "answer-only") {
420
+ return "PREVIEW_ONLY";
421
+ }
422
+ if (changedFiles.length > 0) {
423
+ if (!validationObserved) return "NOT_DELIVERED";
424
+ return claimedOutcome === "ALREADY_SATISFIED" ? "NOT_DELIVERED" : "CHANGED";
425
+ }
426
+ if (mutationIntent === "write" && claimedOutcome === "ALREADY_SATISFIED" && validationObserved) {
427
+ return "ALREADY_SATISFIED";
428
+ }
429
+ return "NOT_DELIVERED";
430
+ }
431
+ function decideTestAction({
432
+ executableBehavior,
433
+ changedFiles = [],
434
+ addedFiles = [],
435
+ deletedFiles = [],
436
+ passingBehaviorTest
437
+ }) {
438
+ if (!executableBehavior) {
439
+ return { action: "none", reason: "No executable behavior changed; automated behavior-test changes are not required." };
440
+ }
441
+ if (!passingBehaviorTest) {
442
+ return { action: "none", reason: "Executable behavior changed, but no proportional automated behavior test passed." };
443
+ }
444
+ const normalizePath2 = (file) => file.replaceAll("\\", "/");
445
+ const deleted = new Set(deletedFiles.map(normalizePath2));
446
+ const added = new Set(addedFiles.map(normalizePath2));
447
+ const deletedTests = changedFiles.filter((file) => TEST_FILE_PATTERN.test(file) && deleted.has(normalizePath2(file)));
448
+ const changedTests = changedFiles.filter((file) => TEST_FILE_PATTERN.test(file) && !deleted.has(normalizePath2(file)));
449
+ if (changedTests.some((file) => added.has(normalizePath2(file)))) {
450
+ return { action: "add", reason: "A new automated behavior test was added for behavior without relevant existing coverage." };
451
+ }
452
+ if (changedTests.length > 0) {
453
+ return { action: "extend", reason: "Existing automated tests were extended to cover the changed behavior invariant." };
454
+ }
455
+ if (deletedTests.length > 0) {
456
+ return { action: "reuse", reason: "Deleted test files do not count as extended coverage; the remaining automated behavior tests passed." };
457
+ }
458
+ return { action: "reuse", reason: "Existing automated behavior tests passed without requiring new test files." };
459
+ }
460
+
461
+ // src/core/validation/evidence-collector.ts
377
462
  var VALIDATION_KINDS = /* @__PURE__ */ new Set(["test", "build", "typecheck", "lint", "security", "smoke", "validate", "other"]);
378
463
  function publicStatus(status) {
379
464
  if (status === "PASS") return "COMPLETED";
380
465
  if (status === "PASS_WITH_NOTES") return "COMPLETED_WITH_NOTES";
381
466
  return "BLOCKED";
382
467
  }
468
+ function blockingDiagnostic(gate, observed, expected, nextAction) {
469
+ return `Gate: ${gate}; Observed: ${observed}; Expected: ${expected}; Next action: ${nextAction}`;
470
+ }
383
471
  function inferTaskKind(task = {}) {
384
472
  const explicit = String(task.kind || "").trim().toLowerCase();
385
473
  if (VALIDATION_KINDS.has(explicit)) return explicit;
@@ -408,6 +496,11 @@ var EvidenceCollector = class {
408
496
  visualDist;
409
497
  port;
410
498
  workflowEvidence;
499
+ claimedOutcome;
500
+ deliveryChangedFiles;
501
+ deliveryAddedFiles;
502
+ deliveryDeletedFiles;
503
+ riskLevel;
411
504
  constructor({
412
505
  cwd,
413
506
  maxLogLength = 2e3,
@@ -421,7 +514,12 @@ var EvidenceCollector = class {
421
514
  explicitApprovals = [],
422
515
  visualDist = null,
423
516
  port = 8080,
424
- workflowEvidence = null
517
+ workflowEvidence = null,
518
+ claimedOutcome = null,
519
+ deliveryChangedFiles = null,
520
+ deliveryAddedFiles = null,
521
+ deliveryDeletedFiles = null,
522
+ riskLevel = null
425
523
  }) {
426
524
  this.cwd = cwd;
427
525
  this.maxLogLength = maxLogLength;
@@ -436,6 +534,11 @@ var EvidenceCollector = class {
436
534
  this.visualDist = visualDist;
437
535
  this.port = Number(port || 8080);
438
536
  this.workflowEvidence = workflowEvidence;
537
+ this.claimedOutcome = claimedOutcome;
538
+ this.deliveryChangedFiles = deliveryChangedFiles;
539
+ this.deliveryAddedFiles = deliveryAddedFiles;
540
+ this.deliveryDeletedFiles = deliveryDeletedFiles;
541
+ this.riskLevel = riskLevel || (this.evidencePolicy === "required" ? "high" : "medium");
439
542
  }
440
543
  async findUserRequest() {
441
544
  if (this.userRequest) return this.userRequest;
@@ -473,8 +576,8 @@ var EvidenceCollector = class {
473
576
  try {
474
577
  const { execSync: execSync2 } = await import("child_process");
475
578
  const branch = execSync2("git branch --show-current", { cwd: this.cwd, encoding: "utf8" }).trim();
476
- if (branch && branch !== "main" && branch !== "master" && branch.startsWith("feat/")) {
477
- return branch.replace("feat/", "").replaceAll("-", " ");
579
+ if (branch && branch !== "main" && branch !== "master" && /^(feat|fix|docs|chore)\//.test(branch)) {
580
+ return branch.replace(/^(feat|fix|docs|chore)\//, "").replaceAll("-", " ");
478
581
  }
479
582
  } catch {
480
583
  }
@@ -482,6 +585,29 @@ var EvidenceCollector = class {
482
585
  }
483
586
  runTask(task) {
484
587
  const kind = inferTaskKind(task);
588
+ if (task.internal === "document-integrity") {
589
+ const failures = [];
590
+ for (const file of task.files || []) {
591
+ try {
592
+ const content = fsSync.readFileSync(path3.join(this.cwd, file), "utf8");
593
+ if (!content.trim()) failures.push(`${file}: empty file`);
594
+ if (/^(<<<<<<<|=======|>>>>>>>)/m.test(content)) failures.push(`${file}: conflict marker`);
595
+ if (content.split(/\r?\n/).some((line) => /[ \t]+$/.test(line))) failures.push(`${file}: trailing whitespace`);
596
+ } catch {
597
+ failures.push(`${file}: unreadable`);
598
+ }
599
+ }
600
+ const status2 = failures.length ? "FAIL" : "PASS";
601
+ return {
602
+ name: task.name,
603
+ command: task.command,
604
+ kind,
605
+ status: status2,
606
+ exitCode: status2 === "PASS" ? 0 : 1,
607
+ summary: status2 === "PASS" ? "Documentation integrity checks passed." : failures.join("; "),
608
+ output: failures.join("\n")
609
+ };
610
+ }
485
611
  if (task.presetStatus) {
486
612
  return { name: task.name, command: task.command, kind, status: task.presetStatus, exitCode: null, summary: task.summary || "", output: "" };
487
613
  }
@@ -523,15 +649,42 @@ var EvidenceCollector = class {
523
649
  ... [TRUNCATED]` : rawOutput;
524
650
  return { name: task.name, command: task.command, kind, status, exitCode: result.status, signal: result.signal, summary, output };
525
651
  }
652
+ findAddedFiles(changedFiles) {
653
+ const added = /* @__PURE__ */ new Set();
654
+ for (const args of [
655
+ ["diff", "--cached", "--diff-filter=A", "--name-only"],
656
+ ["ls-files", "--others", "--exclude-standard"]
657
+ ]) {
658
+ const result = spawnSync("git", args, { cwd: this.cwd, encoding: "utf8" });
659
+ if (result.status !== 0) continue;
660
+ String(result.stdout || "").split("\n").map((file) => file.trim().replaceAll("\\", "/")).filter(Boolean).forEach((file) => added.add(file));
661
+ }
662
+ return changedFiles.filter((file) => added.has(file.replaceAll("\\", "/")));
663
+ }
526
664
  async collect(tasks, { writeArtifact = false } = {}) {
527
- const qualityGuard = new QualityGuard({ cwd: this.cwd, taskSlug: this.taskSlug, evidencePolicy: this.evidencePolicy, riskLevel: this.evidencePolicy === "required" ? "high" : "medium", evidenceWillBePersisted: writeArtifact });
528
- const implementation = qualityGuard.isImplementationTask();
529
- const executableBehavior = await qualityGuard.hasExecutableBehaviorChanges();
665
+ const qualityGuard = new QualityGuard({
666
+ cwd: this.cwd,
667
+ taskSlug: this.taskSlug,
668
+ evidencePolicy: this.evidencePolicy,
669
+ riskLevel: this.riskLevel,
670
+ evidenceWillBePersisted: writeArtifact,
671
+ scopedChangedFiles: this.deliveryChangedFiles
672
+ });
673
+ const implementationFilesPresent = qualityGuard.isImplementationTask();
674
+ const executableBehavior = await qualityGuard.hasExecutableBehaviorChanges(this.deliveryChangedFiles || void 0);
530
675
  const results = tasks.map((task) => this.runTask(task));
531
676
  const policyValidation = await qualityGuard.verify();
532
677
  const behaviorTests = results.filter((result) => result.kind === "test");
533
678
  const passingBehaviorTest = behaviorTests.some((result) => result.status === "PASS");
679
+ const behaviorTestRequired = executableBehavior && (behaviorTests.length > 0 || this.profile.startsWith("frontend") || qualityGuard.isUiTask());
680
+ const validationObserved = results.some(
681
+ (result) => result.kind !== "other" && ["PASS", "PASS_WITH_NOTES"].includes(result.status)
682
+ );
534
683
  const userRequest = await this.findUserRequest();
684
+ const requestRouter = new RequestRouter({ cwd: this.cwd });
685
+ const requestUnderstanding = userRequest ? requestRouter.understand(userRequest) : null;
686
+ const routingDecision = requestUnderstanding ? requestRouter.route(requestUnderstanding) : null;
687
+ const implementation = requestUnderstanding ? requestUnderstanding.deliveryMode === "workspace" && requestUnderstanding.mutationIntent !== "readonly" : implementationFilesPresent;
535
688
  const engine = new DeliveryDecisionEngine({ cwd: this.cwd });
536
689
  const projectContext = await engine.detectContext();
537
690
  const deliveryDecision = await engine.determineDecision({
@@ -546,16 +699,18 @@ var EvidenceCollector = class {
546
699
  explicitApprovals: this.explicitApprovals || []
547
700
  });
548
701
  const changedFiles = policyValidation.git?.changedFiles || [];
702
+ const deliveryChangedFiles = this.deliveryChangedFiles || changedFiles;
703
+ const addedFiles = this.deliveryAddedFiles || this.findAddedFiles(deliveryChangedFiles);
549
704
  const artifactFidelityCheck = await fidelityGate.verifyRequestFidelity({
550
705
  userRequest,
551
706
  deliveryDecision,
552
707
  projectContext,
553
- changedFiles
708
+ changedFiles: deliveryChangedFiles
554
709
  });
555
710
  let overallStatus = "PASS";
556
- if (implementation && executableBehavior && tasks.length === 0) overallStatus = "BLOCKED";
557
- else if (results.some((result) => ["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status))) overallStatus = "FAIL_QUALITY_GATE";
558
- else if (executableBehavior && !passingBehaviorTest) overallStatus = "BLOCKED";
711
+ if (results.some((result) => ["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status))) overallStatus = "FAIL_QUALITY_GATE";
712
+ else if (implementation && deliveryChangedFiles.length > 0 && !validationObserved) overallStatus = "BLOCKED";
713
+ else if (behaviorTestRequired && !passingBehaviorTest) overallStatus = "BLOCKED";
559
714
  else if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(policyValidation.overallStatus)) overallStatus = "FAIL_QUALITY_GATE";
560
715
  else if (results.some((result) => result.status === "PASS_WITH_NOTES") || policyValidation.overallStatus === "PASS_WITH_NOTES") overallStatus = "PASS_WITH_NOTES";
561
716
  if (!artifactFidelityCheck || !artifactFidelityCheck.passed) {
@@ -567,14 +722,95 @@ var EvidenceCollector = class {
567
722
  overallStatus = "BLOCKED";
568
723
  }
569
724
  }
725
+ const testDecision = decideTestAction({
726
+ executableBehavior,
727
+ changedFiles: deliveryChangedFiles,
728
+ addedFiles,
729
+ deletedFiles: this.deliveryDeletedFiles || [],
730
+ passingBehaviorTest
731
+ });
732
+ const deliveryMode = requestUnderstanding?.deliveryMode || (implementation ? "workspace" : "answer-only");
733
+ const mutationIntent = requestUnderstanding?.mutationIntent || (implementation ? "write" : "readonly");
734
+ const deliveryOutcome = resolveDeliveryOutcome({
735
+ deliveryMode,
736
+ mutationIntent,
737
+ changedFiles: deliveryChangedFiles,
738
+ validationStatus: overallStatus,
739
+ validationObserved,
740
+ deliveryDecision: deliveryDecision.decision,
741
+ claimedOutcome: this.claimedOutcome
742
+ });
743
+ if (["NEEDS_CLARIFICATION", "NOT_DELIVERED"].includes(deliveryOutcome) && ["PASS", "PASS_WITH_NOTES"].includes(overallStatus)) {
744
+ overallStatus = "BLOCKED";
745
+ }
570
746
  const limitations = [];
571
- if (implementation && executableBehavior && tasks.length === 0) limitations.push("No meaningful validation command was available for executable implementation work.");
572
- if (executableBehavior && behaviorTests.length === 0) {
573
- limitations.push("Executable behavior changed without a proportional automated behavior test. Build, lint, typecheck, smoke, screenshots, and manual review do not replace behavior tests.");
747
+ if (implementation && deliveryChangedFiles.length > 0 && !validationObserved) limitations.push(blockingDiagnostic(
748
+ "validation",
749
+ "the requested workspace changed but no meaningful validation result was observed",
750
+ "at least one proportional validation command must pass after the change",
751
+ "run the relevant check and finalize again"
752
+ ));
753
+ if (implementation && executableBehavior && tasks.length === 0) limitations.push(blockingDiagnostic(
754
+ "validation-path",
755
+ "executable implementation work has no available validation command",
756
+ "the project must provide a proportional syntax, build, or behavior check",
757
+ "configure or run the smallest relevant validator"
758
+ ));
759
+ if (behaviorTestRequired && behaviorTests.length === 0) {
760
+ limitations.push(blockingDiagnostic(
761
+ "behavior-test",
762
+ "UI or test-capable executable behavior changed without an automated behavior test",
763
+ "a relevant automated behavior test must pass",
764
+ "run or configure the focused behavior test; build, lint, and screenshots are not substitutes"
765
+ ));
766
+ }
767
+ for (const result of results) {
768
+ if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status)) {
769
+ limitations.push(blockingDiagnostic(
770
+ `command:${result.name}`,
771
+ `${result.summary || result.status} (${result.status})`,
772
+ `${Array.isArray(result.command) ? result.command.join(" ") : result.command} must exit successfully`,
773
+ "correct the failure and rerun this command"
774
+ ));
775
+ }
574
776
  }
575
777
  for (const [name, check] of Object.entries(policyValidation.checks || {})) {
576
778
  if (check.status === "PASS_WITH_NOTES" && check.reason) limitations.push(`${name}: ${check.reason}`);
779
+ if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(check.status)) {
780
+ limitations.push(blockingDiagnostic(
781
+ `quality:${name}`,
782
+ check.reason || check.status,
783
+ `${name} must pass under the active risk/evidence policy`,
784
+ "satisfy the stated quality requirement and finalize again"
785
+ ));
786
+ }
787
+ }
788
+ if (!artifactFidelityCheck?.passed) {
789
+ const detail = artifactFidelityCheck?.reason || artifactFidelityCheck?.violations?.join("; ") || "the observed artifact does not match the requested delivery shape";
790
+ limitations.push(blockingDiagnostic(
791
+ "artifact-fidelity",
792
+ detail,
793
+ "the delivered artifact must match the requested stack and shape",
794
+ "correct the artifact or clarify the requested target"
795
+ ));
796
+ }
797
+ if (implementation && (!deliveryDecision?.decision || deliveryDecision.decision.startsWith("BLOCK") || deliveryDecision.decision.includes("BLOCKED") || deliveryDecision.decision === "REQUIRE_EXPLICIT_AUTHORIZATION" || deliveryDecision.decision === "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL")) {
798
+ limitations.push(blockingDiagnostic(
799
+ "delivery-decision",
800
+ deliveryDecision?.reason || deliveryDecision?.decision || "no delivery decision was produced",
801
+ "the delivery decision must authorize this workspace outcome",
802
+ "satisfy the stated decision requirement before finalizing"
803
+ ));
804
+ }
805
+ if (["NEEDS_CLARIFICATION", "NOT_DELIVERED"].includes(deliveryOutcome)) {
806
+ limitations.push(blockingDiagnostic(
807
+ "delivery-outcome",
808
+ `the resolved outcome is ${deliveryOutcome}`,
809
+ "a workspace request requires a scoped change or verified ALREADY_SATISFIED result plus proportional validation",
810
+ "complete the missing delivery condition or request clarification"
811
+ ));
577
812
  }
813
+ const uniqueLimitations = [...new Set(limitations)];
578
814
  let visualEvidence = null;
579
815
  if (this.visualDist) {
580
816
  console.log(`
@@ -603,11 +839,18 @@ var EvidenceCollector = class {
603
839
  branchRecovery: this.branchRecovery,
604
840
  branch: policyValidation.git?.branch || "unknown",
605
841
  changedFiles,
842
+ deliveryChangedFiles,
843
+ mutationOwner: routingDecision?.mutationOwner || null,
844
+ capabilitiesUsed: requestUnderstanding?.capabilityRoles || [],
845
+ deliveryMode,
846
+ deliveryOutcome,
847
+ testAction: testDecision.action,
848
+ testReason: testDecision.reason,
606
849
  status: publicStatus(overallStatus),
607
850
  internalStatus: overallStatus,
608
851
  commands: results,
609
852
  checks: policyValidation.checks,
610
- limitations,
853
+ limitations: uniqueLimitations,
611
854
  deliveryDecision,
612
855
  artifactFidelityCheck,
613
856
  visualEvidence,
@@ -627,6 +870,7 @@ var EvidenceCollector = class {
627
870
  // src/core/validation/validation-planner.ts
628
871
  import fs4 from "fs/promises";
629
872
  import path4 from "path";
873
+ import { spawnSync as spawnSync2 } from "child_process";
630
874
  var ValidationPlanner = class {
631
875
  cwd;
632
876
  qualityGuard;
@@ -636,26 +880,80 @@ var ValidationPlanner = class {
636
880
  this.qualityGuard = qualityGuard;
637
881
  this.stackDetector = new StackDetector({ cwd });
638
882
  }
639
- async plan(profile = "generic") {
883
+ async plan(profile = "generic", { requireObservedValidation = false } = {}) {
640
884
  const stacks = await this.stackDetector.detect();
641
885
  const tasks = [];
642
- const changedFiles = this.qualityGuard.getChangedFiles().filter((file) => {
886
+ const sourceFiles = typeof this.qualityGuard.getScopedChangedFiles === "function" ? this.qualityGuard.getScopedChangedFiles() : this.qualityGuard.getChangedFiles();
887
+ const changedFiles = sourceFiles.filter((file) => {
643
888
  return !/(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(String(file).replaceAll("\\", "/"));
644
889
  });
645
890
  const executableBehavior = await this.qualityGuard.hasExecutableBehaviorChanges();
891
+ const highRisk = this.qualityGuard.riskLevel === "high";
892
+ const noChangeVerification = changedFiles.length === 0 && requireObservedValidation && !highRisk;
893
+ const docsOnly = changedFiles.length > 0 && changedFiles.every(
894
+ (file) => /(^|\/)(docs?|readme|changelog)(\/|\.|$)|\.(md|txt)$/i.test(file)
895
+ );
896
+ if (docsOnly || changedFiles.length === 0 && !highRisk && !requireObservedValidation) return tasks;
897
+ const uiChanged = changedFiles.some(
898
+ (file) => /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/i.test(file) || /(^|\/)(components|pages|views)\//i.test(file)
899
+ );
646
900
  if (stacks.includes("node")) {
647
901
  try {
648
902
  const pkg = JSON.parse(await fs4.readFile(path4.join(this.cwd, "package.json"), "utf8"));
649
903
  const scripts = pkg.scripts || {};
650
- this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
651
- this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
652
- this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
653
- this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
654
- if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
655
- this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
656
- }
657
904
  const hasTs = changedFiles.some((file) => /\.(ts|tsx)$/.test(file)) || await fs4.access(path4.join(this.cwd, "tsconfig.json")).then(() => true).catch(() => false);
658
- if (hasTs && !scripts.typecheck) {
905
+ const addValidate = () => {
906
+ if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
907
+ this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
908
+ }
909
+ };
910
+ if (noChangeVerification) {
911
+ if (scripts.test && !this.isNoOpScript(scripts.test)) {
912
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"]);
913
+ } else if (scripts.typecheck) {
914
+ this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
915
+ } else if (scripts.lint) {
916
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
917
+ } else if (scripts.build) {
918
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
919
+ } else {
920
+ addValidate();
921
+ }
922
+ } else if (highRisk) {
923
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
924
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
925
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
926
+ this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
927
+ addValidate();
928
+ } else if (executableBehavior) {
929
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: true });
930
+ if (uiChanged) {
931
+ if (scripts.build) {
932
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
933
+ } else {
934
+ tasks.push({
935
+ name: "build",
936
+ command: ["npm", "run", "build"],
937
+ kind: "build",
938
+ presetStatus: "FAIL_QUALITY_GATE",
939
+ summary: "Executable UI change requires a configured build script."
940
+ });
941
+ }
942
+ } else if (hasTs) {
943
+ if (scripts.build) this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
944
+ else this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
945
+ }
946
+ } else if (uiChanged) {
947
+ if (scripts.build) this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
948
+ else this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
949
+ } else if (scripts.lint) {
950
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
951
+ } else if (scripts.build) {
952
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
953
+ } else {
954
+ addValidate();
955
+ }
956
+ if (hasTs && !scripts.typecheck && !scripts.build && !noChangeVerification) {
659
957
  tasks.push({
660
958
  name: "typecheck",
661
959
  command: ["npm", "run", "typecheck"],
@@ -676,9 +974,10 @@ var ValidationPlanner = class {
676
974
  kind: "lint"
677
975
  });
678
976
  }
679
- if (executableBehavior) {
977
+ if (executableBehavior || changedFiles.length === 0 && requireObservedValidation && tasks.length === 0) {
680
978
  const hasPytestConfig = await fs4.access(path4.join(this.cwd, "pytest.ini")).then(() => true).catch(() => false) || await fs4.access(path4.join(this.cwd, "pyproject.toml")).then(() => true).catch(() => false);
681
- if (hasPytestConfig || pythonFiles.length > 0) {
979
+ const pytestAvailable = highRisk && spawnCommandAvailable("pytest", this.cwd);
980
+ if (hasPytestConfig || pytestAvailable) {
682
981
  tasks.push({
683
982
  name: "python-test",
684
983
  command: ["pytest"],
@@ -698,9 +997,10 @@ var ValidationPlanner = class {
698
997
  });
699
998
  }
700
999
  }
701
- if (executableBehavior) {
1000
+ if (executableBehavior || changedFiles.length === 0 && requireObservedValidation && tasks.length === 0) {
702
1001
  const hasPhpUnitXml = await fs4.access(path4.join(this.cwd, "phpunit.xml")).then(() => true).catch(() => false) || await fs4.access(path4.join(this.cwd, "phpunit.xml.dist")).then(() => true).catch(() => false);
703
- if (hasPhpUnitXml || phpFiles.length > 0) {
1002
+ const localPhpUnit = await fs4.access(path4.join(this.cwd, "vendor/bin/phpunit")).then(() => true).catch(() => false);
1003
+ if (hasPhpUnitXml || highRisk && localPhpUnit) {
704
1004
  tasks.push({
705
1005
  name: "php-test",
706
1006
  command: ["vendor/bin/phpunit"],
@@ -732,11 +1032,16 @@ var ValidationPlanner = class {
732
1032
  return /^(echo\b|true$|exit\s+0$|node\s+-e\s+["']?console\.log)/.test(value) || value.includes("tests-pass");
733
1033
  }
734
1034
  };
1035
+ function spawnCommandAvailable(command, cwd) {
1036
+ const lookup = process.platform === "win32" ? ["where", [command]] : ["which", [command]];
1037
+ return spawnSync2(lookup[0], lookup[1], { cwd, stdio: "ignore" }).status === 0;
1038
+ }
735
1039
 
736
1040
  export {
737
1041
  QualityGuard,
738
1042
  VisualVerifier,
1043
+ parseDeliveryOutcomeClaim,
739
1044
  EvidenceCollector,
740
1045
  ValidationPlanner
741
1046
  };
742
- //# sourceMappingURL=chunk-Y4RLP6ZM.js.map
1047
+ //# sourceMappingURL=chunk-LD7EHAU2.js.map
package/core/index.d.ts CHANGED
@@ -1,11 +1,16 @@
1
+ type DeliveryMode = "workspace" | "answer-only";
2
+
3
+ type MutationOwner = "Astra" | "Phoenix" | "ControlPlane" | null;
1
4
  interface RequestUnderstanding {
2
5
  rawRequest: string;
3
6
  taskGoal: string;
4
7
  requestedActor?: string;
5
8
  invalidActor?: string;
6
9
  requestedCapability?: string;
10
+ capabilityRoles: string[];
7
11
  operationType: string;
8
12
  mutationIntent: "readonly" | "write" | "publish";
13
+ deliveryMode: DeliveryMode;
9
14
  riskLevel: "low" | "medium" | "high";
10
15
  requiredEvidence: string[];
11
16
  safetyConstraints: string[];
@@ -16,8 +21,11 @@ interface RequestUnderstanding {
16
21
  interface RoutingDecision {
17
22
  requestedActor?: string;
18
23
  selectedActor?: string;
24
+ mutationOwner: MutationOwner;
25
+ capabilityRoles: string[];
19
26
  operationType: string;
20
27
  mutationIntent: "readonly" | "write" | "publish";
28
+ deliveryMode: DeliveryMode;
21
29
  permissionDecision: "allowed" | "rerouted" | "blocked";
22
30
  reason: string;
23
31
  workflowPath: string[];
@@ -176,11 +184,20 @@ interface ExecuteOptions {
176
184
  readOnly?: boolean;
177
185
  fastTrack?: boolean;
178
186
  requireActorConfirmation?: boolean;
187
+ orchestratedChild?: boolean;
188
+ }
189
+ interface ObservedCommand {
190
+ command: string;
191
+ tool: string;
192
+ status: "PASS" | "FAIL" | "UNKNOWN";
193
+ exitCode: number | null;
194
+ source: "sdk" | "json-event" | "text-output";
179
195
  }
180
196
  interface ExecuteResult {
181
197
  success: boolean;
182
198
  status?: string;
183
199
  commandsRun: string[];
200
+ commandEvents?: ObservedCommand[];
184
201
  eventCount: number;
185
202
  error?: string;
186
203
  runtimeAgentRequested?: string | null;
@@ -205,7 +222,7 @@ declare class OpenCodeAdapter {
205
222
  /**
206
223
  * Runs opencode with a prompt and options.
207
224
  */
208
- execute(message: string, { agent, model, readOnly, fastTrack, requireActorConfirmation }?: ExecuteOptions): Promise<ExecuteResult>;
225
+ execute(message: string, { agent, model, readOnly, fastTrack, requireActorConfirmation, orchestratedChild }?: ExecuteOptions): Promise<ExecuteResult>;
209
226
  }
210
227
 
211
228
  interface DelegationControllerOptions {
@@ -351,6 +368,8 @@ declare class WorkspaceSnapshot {
351
368
  });
352
369
  isIgnored(relativePath: string): boolean;
353
370
  capture(): Promise<WorkspaceSnapshotData>;
371
+ listGitVisibleFiles(): string[] | null;
372
+ scanFiles(relativePaths: string[], filesMap: Record<string, SnapshotFile>): Promise<void>;
354
373
  scanDir(dir: string, filesMap: Record<string, SnapshotFile>): Promise<void>;
355
374
  calculateHash(snapshot: WorkspaceSnapshotData): string;
356
375
  }