@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.
- package/CHANGELOG.md +41 -0
- package/bin/ai-workflow.js +193 -49
- package/bin/ai-workflow.js.map +1 -1
- package/{chunk-LI76KI7C.js → chunk-4FI5ODAM.js} +259 -296
- package/{chunk-XW747GIG.js → chunk-H7GIKXFO.js} +339 -14
- package/{chunk-Y4RLP6ZM.js → chunk-LD7EHAU2.js} +343 -38
- package/core/index.d.ts +20 -1
- package/core/index.js +2 -2
- package/dist-assets/agents/atlas.md +25 -6
- package/dist-assets/commands/atlas.md +7 -2
- package/dist-assets/docs/QUICKSTART.md +10 -1
- package/dist-assets/docs/compatibility/provider-usage.md +16 -1
- package/dist-assets/docs/compatibility/runtime-matrix.md +13 -2
- package/dist-assets/docs/policies/06-FINAL_EVIDENCE_CONTRACT.md +11 -1
- package/dist-assets/docs/policies/SKILLS_COMMON_GOVERNANCE.md +16 -0
- package/dist-assets/schemas/README.md +4 -0
- package/dist-assets/schemas/evidence.schema.json +60 -1
- package/package.json +1 -1
- package/{validate-IEHLAVZ6.js → validate-F3ZH63LI.js} +3 -3
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
ArtifactFidelityGate,
|
|
3
3
|
DeliveryDecisionEngine,
|
|
4
4
|
MergeGate,
|
|
5
|
+
RequestRouter,
|
|
5
6
|
StackDetector
|
|
6
|
-
} from "./chunk-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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" &&
|
|
477
|
-
return branch.replace(
|
|
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({
|
|
528
|
-
|
|
529
|
-
|
|
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 (
|
|
557
|
-
else if (
|
|
558
|
-
else if (
|
|
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 &&
|
|
572
|
-
|
|
573
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
}
|