wogiflow 2.7.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/wogi-audit.md +156 -8
- package/.claude/commands/wogi-start.md +276 -0
- package/lib/workspace-channel-server.js +19 -4
- package/lib/workspace-gates.js +87 -0
- package/lib/workspace-routing.js +15 -5
- package/lib/workspace-session.js +308 -0
- package/lib/workspace.js +49 -3
- package/package.json +1 -1
- package/scripts/flow-audit-gates.js +766 -0
- package/scripts/flow-config-defaults.js +27 -4
- package/scripts/flow-config-migrate.js +270 -0
- package/scripts/flow-context-manifest.js +322 -0
- package/scripts/flow-done-gates.js +76 -0
- package/scripts/flow-done.js +14 -0
- package/scripts/flow-gate-latch.js +119 -0
- package/scripts/flow-runtime-verification.js +782 -0
- package/scripts/hooks/core/post-compact.js +11 -1
- package/scripts/hooks/core/session-context.js +51 -7
- package/scripts/hooks/core/task-completed.js +26 -0
- package/scripts/postinstall.js +20 -0
|
@@ -721,6 +721,80 @@ function testDiscoveryGate(ctx) {
|
|
|
721
721
|
}
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Verification proof gate — checks that completed acceptance criteria
|
|
726
|
+
* have verification evidence recorded in the durable session.
|
|
727
|
+
*
|
|
728
|
+
* Without this, agents can mark criteria as "completed" without any
|
|
729
|
+
* behavioral evidence that the feature actually works.
|
|
730
|
+
*
|
|
731
|
+
* Checks durable-session.json for verificationProof on each completed step.
|
|
732
|
+
* Steps without proof are flagged. If ALL steps lack proof, gate blocks.
|
|
733
|
+
* If SOME steps have proof, gate warns (partial evidence).
|
|
734
|
+
*/
|
|
735
|
+
function verificationProofGate(ctx) {
|
|
736
|
+
let loadDurableSession;
|
|
737
|
+
try {
|
|
738
|
+
({ loadDurableSession } = require('./flow-durable-session'));
|
|
739
|
+
} catch (_err) {
|
|
740
|
+
ctx.warn('verificationProof (durable session module not available)');
|
|
741
|
+
return { passed: true };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
const session = loadDurableSession();
|
|
746
|
+
if (!session || !session.taskId) {
|
|
747
|
+
// No durable session — graceful fallback for tasks created before this gate existed
|
|
748
|
+
console.log(` ${ctx.color('yellow', '\u25CB')} verificationProof (no durable session — skipping)`);
|
|
749
|
+
return { passed: true };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Only check acceptance criteria steps, not system steps.
|
|
753
|
+
// Normalize step type to handle both kebab-case and snake_case variants.
|
|
754
|
+
const normalizeStepType = (type) => (type || '').toLowerCase().replace(/_/g, '-');
|
|
755
|
+
const criteriaSteps = (session.steps || []).filter(s =>
|
|
756
|
+
s.status === 'completed' && normalizeStepType(s.type) === 'acceptance-criteria'
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
if (criteriaSteps.length === 0) {
|
|
760
|
+
console.log(` ${ctx.color('yellow', '\u25CB')} verificationProof (no completed criteria — skipping)`);
|
|
761
|
+
return { passed: true };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const unverified = criteriaSteps.filter(s => !s.verificationProof);
|
|
765
|
+
const verified = criteriaSteps.length - unverified.length;
|
|
766
|
+
|
|
767
|
+
if (unverified.length === 0) {
|
|
768
|
+
ctx.success(`verificationProof (${verified}/${criteriaSteps.length} criteria have evidence)`);
|
|
769
|
+
return { passed: true };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// If ALL criteria lack proof → hard block
|
|
773
|
+
if (verified === 0) {
|
|
774
|
+
ctx.error(`verificationProof (0/${criteriaSteps.length} criteria have verification evidence)`);
|
|
775
|
+
for (const s of unverified.slice(0, 5)) {
|
|
776
|
+
console.log(ctx.color('dim', ` - ${(s.description || s.title || s.id || '').substring(0, 100)}`));
|
|
777
|
+
}
|
|
778
|
+
console.log(ctx.color('dim', ' Run runtime verification or provide behavioral evidence for each criterion.'));
|
|
779
|
+
return {
|
|
780
|
+
passed: false,
|
|
781
|
+
errorOutput: `${unverified.length} acceptance criteria completed without verification proof. ` +
|
|
782
|
+
'Each criterion needs behavioral evidence (WebMCP, Playwright, curl, or manual checklist).'
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Partial proof — warn but allow (transitional)
|
|
787
|
+
console.log(` ${ctx.color('yellow', '\u25CB')} verificationProof (${verified}/${criteriaSteps.length} verified — ${unverified.length} missing proof)`);
|
|
788
|
+
for (const s of unverified.slice(0, 3)) {
|
|
789
|
+
console.log(ctx.color('dim', ` - Missing: ${(s.description || s.title || s.id || '').substring(0, 80)}`));
|
|
790
|
+
}
|
|
791
|
+
return { passed: true };
|
|
792
|
+
} catch (err) {
|
|
793
|
+
ctx.warn(`verificationProof (error: ${ctx.truncateOutput(err.message, 3, 200)})`);
|
|
794
|
+
return { passed: true };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
724
798
|
function unknownGate(ctx, gateName) {
|
|
725
799
|
console.log(` ${ctx.color('yellow', '\u25CB')} ${gateName} (manual check)`);
|
|
726
800
|
return { passed: true };
|
|
@@ -804,6 +878,7 @@ const GATE_REGISTRY = {
|
|
|
804
878
|
uiVerification: verificationGate,
|
|
805
879
|
apiVerification: verificationGate,
|
|
806
880
|
testDiscovery: testDiscoveryGate,
|
|
881
|
+
verificationProof: verificationProofGate,
|
|
807
882
|
// Workspace gates (conditional — auto-skip when not in workspace)
|
|
808
883
|
workspaceCompliance: workspaceGate,
|
|
809
884
|
};
|
|
@@ -851,6 +926,7 @@ module.exports = {
|
|
|
851
926
|
generatedTestsPassGate,
|
|
852
927
|
verificationGate,
|
|
853
928
|
testDiscoveryGate,
|
|
929
|
+
verificationProofGate,
|
|
854
930
|
workspaceGate,
|
|
855
931
|
unknownGate,
|
|
856
932
|
};
|
package/scripts/flow-done.js
CHANGED
|
@@ -557,6 +557,20 @@ async function main() {
|
|
|
557
557
|
process.exit(1);
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
// Write gate latch — proves quality gates passed for this task.
|
|
561
|
+
// The TaskCompleted hook checks this latch before allowing completion.
|
|
562
|
+
// Without it, agents can call TaskUpdate and bypass all gates.
|
|
563
|
+
try {
|
|
564
|
+
const { setGateLatch } = require('./flow-gate-latch');
|
|
565
|
+
const gates = getConfig().qualityGates?.[taskTypeForGates]?.require
|
|
566
|
+
?? getConfig().qualityGates?.feature?.require ?? [];
|
|
567
|
+
setGateLatch(taskId, gates);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
if (process.env.DEBUG) {
|
|
570
|
+
console.error(`[flow-done] Gate latch write failed: ${err.message}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
560
574
|
console.log('');
|
|
561
575
|
|
|
562
576
|
// Check if task exists
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Gate Latch
|
|
5
|
+
*
|
|
6
|
+
* Records when quality gates have passed for a task.
|
|
7
|
+
* The TaskCompleted hook checks for this latch before allowing
|
|
8
|
+
* a task to move to recentlyCompleted.
|
|
9
|
+
*
|
|
10
|
+
* Without the latch, agents can call TaskUpdate(status: "completed")
|
|
11
|
+
* and bypass all quality gates. The latch ensures that the only path
|
|
12
|
+
* to completion goes through the quality gate pipeline.
|
|
13
|
+
*
|
|
14
|
+
* Flow:
|
|
15
|
+
* 1. flow-done.js runs quality gates → gates pass → writes latch
|
|
16
|
+
* 2. Agent calls TaskUpdate → TaskCompleted hook fires
|
|
17
|
+
* 3. Hook checks latch → if present and recent → allows completion
|
|
18
|
+
* 4. If no latch → blocks completion with actionable error message
|
|
19
|
+
*
|
|
20
|
+
* Latch file: .workflow/state/.gates-passed.json
|
|
21
|
+
* TTL: 30 minutes (stale latches are ignored)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const { PATHS, safeJsonParse } = require('./flow-utils');
|
|
27
|
+
|
|
28
|
+
/** Latch time-to-live in milliseconds (30 minutes) */
|
|
29
|
+
const LATCH_TTL_MS = 30 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
/** Path to the gate latch file */
|
|
32
|
+
const LATCH_PATH = path.join(PATHS.state, '.gates-passed.json');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record that quality gates have passed for a task.
|
|
36
|
+
* Called by flow-done.js after all gates pass.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} taskId - The task ID that passed gates
|
|
39
|
+
* @param {string[]} gatesPassed - Names of gates that passed
|
|
40
|
+
* @returns {{ written: boolean, path: string }}
|
|
41
|
+
*/
|
|
42
|
+
function setGateLatch(taskId, gatesPassed = []) {
|
|
43
|
+
try {
|
|
44
|
+
const latch = {
|
|
45
|
+
taskId,
|
|
46
|
+
gatesPassed,
|
|
47
|
+
passedAt: new Date().toISOString(),
|
|
48
|
+
pid: process.pid
|
|
49
|
+
};
|
|
50
|
+
fs.writeFileSync(LATCH_PATH, JSON.stringify(latch, null, 2));
|
|
51
|
+
return { written: true, path: LATCH_PATH };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (process.env.DEBUG) {
|
|
54
|
+
console.error(`[gate-latch] Failed to write latch: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
return { written: false, path: LATCH_PATH };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if quality gates have passed for a task.
|
|
62
|
+
* Returns the latch data if valid, null if no latch or expired.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} taskId - The task ID to check
|
|
65
|
+
* @returns {{ valid: boolean, latch: Object|null, reason: string }}
|
|
66
|
+
*/
|
|
67
|
+
function checkGateLatch(taskId) {
|
|
68
|
+
const latch = safeJsonParse(LATCH_PATH, null);
|
|
69
|
+
|
|
70
|
+
if (!latch) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
latch: null,
|
|
74
|
+
reason: 'No gate latch found. Quality gates have not been run for this task.'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check task ID matches
|
|
79
|
+
if (latch.taskId !== taskId) {
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
latch,
|
|
83
|
+
reason: `Gate latch is for task ${latch.taskId}, not ${taskId}.`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check TTL
|
|
88
|
+
const passedAt = new Date(latch.passedAt).getTime();
|
|
89
|
+
const age = Date.now() - passedAt;
|
|
90
|
+
if (age > LATCH_TTL_MS) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
latch,
|
|
94
|
+
reason: `Gate latch expired (${Math.round(age / 60000)} min old, TTL is ${LATCH_TTL_MS / 60000} min).`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
valid: true,
|
|
100
|
+
latch,
|
|
101
|
+
reason: `Gates passed at ${latch.passedAt} (${latch.gatesPassed.length} gates)`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear the gate latch after task completion.
|
|
107
|
+
* Prevents stale latches from persisting.
|
|
108
|
+
*/
|
|
109
|
+
function clearGateLatch() {
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(LATCH_PATH)) {
|
|
112
|
+
fs.unlinkSync(LATCH_PATH);
|
|
113
|
+
}
|
|
114
|
+
} catch (_err) {
|
|
115
|
+
// Non-critical
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { setGateLatch, checkGateLatch, clearGateLatch, LATCH_PATH };
|