wogiflow 2.10.0 → 2.12.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/docs/explore-agents.md +35 -0
- package/.workflow/state/bugfix-scope.json.template +7 -0
- package/.workflow/state/deploy-history.json.template +3 -0
- package/.workflow/state/deploy-routes.json.template +4 -0
- package/.workflow/state/strike-tracker.json.template +3 -0
- package/lib/workspace.js +42 -11
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +56 -0
- package/scripts/flow-deploy-gate.js +335 -0
- package/scripts/flow-deploy-history.js +270 -0
- package/scripts/flow-hook-status.js +7 -1
- package/scripts/flow-mcp-capabilities.js +617 -0
- package/scripts/hooks/core/bugfix-scope-gate.js +427 -0
- package/scripts/hooks/core/deploy-gate.js +655 -0
- package/scripts/hooks/core/git-safety-gate.js +358 -0
- package/scripts/hooks/core/index.js +62 -0
- package/scripts/hooks/core/scope-mutation-gate.js +257 -0
- package/scripts/hooks/core/strike-gate.js +380 -0
- package/scripts/hooks/core/task-completed.js +101 -2
- package/scripts/hooks/entry/claude-code/post-tool-use.js +33 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +137 -0
- package/scripts/postinstall.js +15 -4
|
@@ -226,6 +226,41 @@ Return:
|
|
|
226
226
|
|
|
227
227
|
**Blast-radius artifact**: Results are persisted to `.workflow/state/blast-radius-{taskId}.json` for use by downstream gates (context estimator, standards compliance, workspace dispatch).
|
|
228
228
|
|
|
229
|
+
## MCP Capability Injection (Pre-Launch)
|
|
230
|
+
|
|
231
|
+
Before launching agents, inject MCP capability hints so agents can leverage available MCP tools (CC 2.1.101+ — sub-agents inherit MCP tools from parent session).
|
|
232
|
+
|
|
233
|
+
**Step 1**: Check if capabilities are classified:
|
|
234
|
+
```bash
|
|
235
|
+
node scripts/flow-mcp-capabilities.js check-cache
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Step 2**: If `cache-miss` AND you have MCP tools available (tools starting with `mcp__` in your tool catalog):
|
|
239
|
+
1. Inspect your available `mcp__*` tools
|
|
240
|
+
2. For each tool, classify it into a capability category using the category definitions:
|
|
241
|
+
```bash
|
|
242
|
+
node scripts/flow-mcp-capabilities.js categories
|
|
243
|
+
```
|
|
244
|
+
3. Cache the classifications:
|
|
245
|
+
```bash
|
|
246
|
+
node scripts/flow-mcp-capabilities.js cache '<json>'
|
|
247
|
+
```
|
|
248
|
+
Format: `{ "server-name": { "tools": [{ "name": "mcp__server__tool", "description": "...", "category": "category-id" }] } }`
|
|
249
|
+
|
|
250
|
+
**Step 3**: For each agent, get its role-specific hint and append to the agent prompt:
|
|
251
|
+
```bash
|
|
252
|
+
node scripts/flow-mcp-capabilities.js hint explore-codebase # Agent 1
|
|
253
|
+
node scripts/flow-mcp-capabilities.js hint explore-practices # Agent 2
|
|
254
|
+
node scripts/flow-mcp-capabilities.js hint explore-versions # Agent 3
|
|
255
|
+
node scripts/flow-mcp-capabilities.js hint explore-risk # Agent 4
|
|
256
|
+
node scripts/flow-mcp-capabilities.js hint explore-standards # Agent 5
|
|
257
|
+
node scripts/flow-mcp-capabilities.js hint explore-impact # Agent 6
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
If the hint is non-empty, append it to the agent's prompt. If empty (no relevant MCP tools for that role), skip — the agent works fine without them.
|
|
261
|
+
|
|
262
|
+
**Skip when**: No MCP servers configured (`node scripts/flow-mcp-capabilities.js discover` returns empty), or `config.mcpCapabilities.enabled` is false.
|
|
263
|
+
|
|
229
264
|
## Launching
|
|
230
265
|
|
|
231
266
|
All agents launch in parallel as `Agent(subagent_type=Explore)` calls in a single message. When `config.hybrid.enabled`, use the `model` parameter on each Agent call to route by task type:
|
package/lib/workspace.js
CHANGED
|
@@ -717,20 +717,25 @@ grep -l '"from": "<repo-name>"' .workspace/messages/*.json 2>/dev/null
|
|
|
717
717
|
4. If no message after 30s, check the worker's \`ready.json\` for task status
|
|
718
718
|
5. Once message arrives, read it and present the results to the user
|
|
719
719
|
|
|
720
|
-
**Message format** (what workers write automatically):
|
|
720
|
+
**Message format** (what workers write automatically via the task-completed hook):
|
|
721
721
|
\`\`\`json
|
|
722
722
|
{
|
|
723
|
-
"id": "msg
|
|
723
|
+
"id": "msg-<taskId>-<timestamp>",
|
|
724
724
|
"from": "<repo-name>",
|
|
725
725
|
"to": "manager",
|
|
726
726
|
"type": "task-complete",
|
|
727
727
|
"subject": "Task completed: <title>",
|
|
728
|
-
"body": "**Task**: ...\\n**Files changed**: ...\\n**
|
|
728
|
+
"body": "**Task**: ...\\n**Files changed**: ...\\n**Verification evidence**: ...",
|
|
729
729
|
"taskId": "wf-XXXXXXXX",
|
|
730
|
-
"status": "pending"
|
|
730
|
+
"status": "pending",
|
|
731
|
+
"verified": true,
|
|
732
|
+
"evidenceTier": "Tier 3 (INTERACTIVE)|Tier 2 (OBSERVATIONAL)|unknown",
|
|
733
|
+
"timestamp": "ISO-8601"
|
|
731
734
|
}
|
|
732
735
|
\`\`\`
|
|
733
736
|
|
|
737
|
+
**Trust model**: Messages with \`"verified": true\` went through WogiFlow's quality gates (gate latch check). Freeform curl messages from workers are progress reports, not verified completions — investigate if a worker reports "done" via curl but no structured task-complete message arrives.
|
|
738
|
+
|
|
734
739
|
**After reading a result**: Present the findings to the user. If the task requires follow-up (e.g., bug investigation found the issue in the other repo), dispatch the fix to the appropriate worker.
|
|
735
740
|
|
|
736
741
|
**You are the SINGLE interface for the user.** They should never need to look at worker terminals. Read the messages, synthesize, and present.
|
|
@@ -1128,17 +1133,21 @@ This is NON-OPTIONAL. Every channel-dispatched task MUST end with a reply to the
|
|
|
1128
1133
|
**Talk to PEERS directly** (do NOT go through the manager) when you need:
|
|
1129
1134
|
- API shape/endpoint details from the other repo ("What does POST /customers return?")
|
|
1130
1135
|
- Data model clarifications ("Does the Invoice entity have a lineItems relation?")
|
|
1136
|
+
- **Test credentials or accounts** ("Do you have E2E login credentials?" / "Can you create a test admin user?")
|
|
1137
|
+
- **Shared resources** ("Can you expose a test API token?" / "What's the connection string for staging?")
|
|
1131
1138
|
- Coordination on shared types or contracts
|
|
1132
1139
|
- Confirmation that the other side is ready for integration
|
|
1133
1140
|
- Any technical question that the other repo can answer from its codebase
|
|
1134
1141
|
|
|
1142
|
+
Peers can not only ANSWER questions — they can CREATE resources you need (test accounts, API tokens, seed data, environment configs).
|
|
1143
|
+
|
|
1135
1144
|
**Talk to the MANAGER** (which means the user) only when:
|
|
1136
1145
|
- You have **results to report** (task completed, investigation findings)
|
|
1137
1146
|
- You need a **user decision** (design choice, product question, approval)
|
|
1138
|
-
- You are **stuck** and no peer can help
|
|
1147
|
+
- You are **stuck** and no peer can help (after trying Steps 1-2 below)
|
|
1139
1148
|
- You need to **escalate** something the user should know about
|
|
1140
1149
|
|
|
1141
|
-
**Rule**: If a peer can answer your question,
|
|
1150
|
+
**Rule**: If a peer can answer your question or create what you need, go to the peer. Do NOT route through the manager — the manager doesn't read source code and will just relay your question anyway. Cut out the middleman.
|
|
1142
1151
|
|
|
1143
1152
|
**Peer channels:**
|
|
1144
1153
|
${peerNames.map(p => `- **${p}**: \`curl -s -X POST http://localhost:${channelMembers[p]?.port || '???'} -H "X-Wogi-From: ${name}" -d "your question"\``).join('\n')}
|
|
@@ -1152,15 +1161,37 @@ You are a workspace worker. There is NO human watching your terminal. You MUST o
|
|
|
1152
1161
|
- **Auto-approve all reviews**: If /wogi-review asks for fix options, choose Option 1 (fix all) automatically.
|
|
1153
1162
|
- **Never ask clarifying questions**: If something is ambiguous, make a reasonable decision and note it in your reply to the manager.
|
|
1154
1163
|
|
|
1155
|
-
###
|
|
1164
|
+
### CRITICAL: Stop, Don't Degrade
|
|
1165
|
+
|
|
1166
|
+
**If you cannot verify your work to the required evidence tier, you may NOT mark the task as complete.** Report it as BLOCKED with the specific verification gap.
|
|
1167
|
+
|
|
1168
|
+
- If browser verification is required but you can't log in → BLOCKED (not done)
|
|
1169
|
+
- If API testing is required but the server is unreachable → BLOCKED (not done)
|
|
1170
|
+
- If you need credentials you don't have → resolve via Steps 1-2 below, then continue. If unresolvable → BLOCKED.
|
|
1171
|
+
|
|
1172
|
+
**Overnight runs STOP when verification is impossible.** Never degrade quality to keep the queue moving. A blocked task with honest status is infinitely better than a "completed" task that doesn't work.
|
|
1173
|
+
|
|
1174
|
+
### When You're Blocked — Resolution Protocol
|
|
1175
|
+
|
|
1176
|
+
**Step 1: Self-resolve** — check \`.workspace/state/\` for credentials, configs, tokens, test accounts, and any other shared resources. Also check \`.workspace/messages/\` for prior conversations where the resource may have been mentioned.
|
|
1177
|
+
|
|
1178
|
+
**Step 2: Ask peers** — peers can CREATE what you need (test accounts, API tokens, seed data). Send a direct request:
|
|
1179
|
+
\`curl -s -X POST http://localhost:{peer_port} -H "X-Wogi-From: ${name}" -d "I need E2E test credentials. Do you have them, or can you create a test admin account?"\`
|
|
1180
|
+
|
|
1181
|
+
**Step 3: ONLY THEN escalate** to the manager, including what you already tried:
|
|
1182
|
+
|
|
1183
|
+
To escalate: \`curl -s -X POST http://localhost:${config.channels.managerPort || (config.channels.basePort - 1)} -H "X-Wogi-From: ${name}" -d "## Need Decision: [problem]
|
|
1184
|
+
Checked .workspace/state/: [what was found]
|
|
1185
|
+
Asked peers: [who, what response]
|
|
1186
|
+
Why this needs the owner: [explanation]"\`
|
|
1187
|
+
|
|
1188
|
+
### When to Escalate (After Steps 1-2)
|
|
1156
1189
|
|
|
1157
1190
|
Only send a question to the manager (instead of results) when:
|
|
1158
|
-
- The task requires a **design decision** that could go multiple ways
|
|
1191
|
+
- The task requires a **design decision** that could go multiple ways
|
|
1159
1192
|
- The task would **break an API contract** that other repos depend on
|
|
1160
1193
|
- The task requires **deleting user data** or making irreversible changes
|
|
1161
|
-
-
|
|
1162
|
-
|
|
1163
|
-
To escalate: \`curl -s -X POST http://localhost:${config.channels.managerPort || (config.channels.basePort - 1)} -H "X-Wogi-From: ${name}" -d "## Need Decision: [describe the choice and options]"\`
|
|
1194
|
+
- Steps 1-2 failed and you are genuinely **stuck**
|
|
1164
1195
|
|
|
1165
1196
|
For everything else — just do the work and report results.
|
|
1166
1197
|
|
package/package.json
CHANGED
|
@@ -187,12 +187,61 @@ const CONFIG_DEFAULTS = {
|
|
|
187
187
|
implementationGate: { enabled: true },
|
|
188
188
|
todoWriteGate: { enabled: true, blockImplementationWithoutTask: true },
|
|
189
189
|
routingGate: { enabled: true },
|
|
190
|
+
commitLogGate: { enabled: true },
|
|
190
191
|
loopEnforcement: { enabled: true },
|
|
191
192
|
hypothesisGate: {
|
|
192
193
|
enabled: true,
|
|
193
194
|
_comment_hypothesisGate: 'Blocks premature "fixed"/"should work" claims during bug investigation until hypothesis is verified. Pattern: hypothesis → verify → confirm → communicate.',
|
|
194
195
|
blockedPhrases: ['fixed', 'should work', 'go try', 'go refresh'],
|
|
195
196
|
requireExplicitVerification: true
|
|
197
|
+
},
|
|
198
|
+
deployGate: {
|
|
199
|
+
_comment_deployGate: 'Blocks deploy commands unless a valid HMAC-signed verification artifact exists. Off by default — opt in via `flow deploy-gate init`.',
|
|
200
|
+
enabled: false,
|
|
201
|
+
commands: [],
|
|
202
|
+
sourcePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue', '**/*.svelte', '**/*.css'],
|
|
203
|
+
requireForPriorities: ['P0', 'P1'],
|
|
204
|
+
blockWriteToVerifications: true,
|
|
205
|
+
minVerifiedRoutes: 3,
|
|
206
|
+
rejectLoginOnly: true
|
|
207
|
+
},
|
|
208
|
+
strikeEscalation: {
|
|
209
|
+
_comment_strikeEscalation: 'Mechanical strike counter. Blocks Edit/Write after repeated verification failures on the same task.',
|
|
210
|
+
enabled: true,
|
|
211
|
+
blockThreshold: 2,
|
|
212
|
+
escalateThreshold: 3,
|
|
213
|
+
hardBlockThreshold: 4,
|
|
214
|
+
productionCrashThreshold: 2
|
|
215
|
+
},
|
|
216
|
+
bugfixScope: {
|
|
217
|
+
_comment_bugfixScope: 'Pauses L3 bugfixes after N unique non-test files edited, requiring a scope inventory.',
|
|
218
|
+
enabled: true,
|
|
219
|
+
mode: 'warn',
|
|
220
|
+
fileThreshold: 3,
|
|
221
|
+
excludePatterns: ['*.test.*', '*.spec.*', '*.d.ts', '__tests__/**', '__mocks__/**'],
|
|
222
|
+
keywordMatchThreshold: 2,
|
|
223
|
+
fanOutThreshold: 10
|
|
224
|
+
},
|
|
225
|
+
revertFirst: {
|
|
226
|
+
_comment_revertFirst: 'For production crashes: recommends reverting before forward-fixing. Off by default.',
|
|
227
|
+
enabled: false,
|
|
228
|
+
keywords: ['production', 'crash', 'down', 'outage', '500 errors', 'users can\u0027t', 'site is broken', 'live issue'],
|
|
229
|
+
deployHistoryRetention: 50,
|
|
230
|
+
oldDeployWarningDays: 7
|
|
231
|
+
},
|
|
232
|
+
scopeMutation: {
|
|
233
|
+
_comment_scopeMutation: 'Agnostic gate: fix tasks creating 2+ new files → warn. Deleting pre-existing files → warn. No framework pattern matching.',
|
|
234
|
+
enabled: true,
|
|
235
|
+
newFileThreshold: 2,
|
|
236
|
+
mode: 'warn'
|
|
237
|
+
},
|
|
238
|
+
gitSafety: {
|
|
239
|
+
_comment_gitSafety: 'Auto-backup before destructive git operations. Prevents accidental loss of work from prior sessions.',
|
|
240
|
+
enabled: true,
|
|
241
|
+
maxBackwardCommits: 3,
|
|
242
|
+
ageThresholdHours: 24,
|
|
243
|
+
autoBackup: true,
|
|
244
|
+
maxBackupBranches: 3
|
|
196
245
|
}
|
|
197
246
|
},
|
|
198
247
|
|
|
@@ -342,6 +391,13 @@ const CONFIG_DEFAULTS = {
|
|
|
342
391
|
similarityThreshold: 0.8,
|
|
343
392
|
similarityWarningThreshold: 0.6
|
|
344
393
|
},
|
|
394
|
+
// --- Workspace Sovereignty ---
|
|
395
|
+
workspace: {
|
|
396
|
+
_comment_workerGatesSovereign: 'When true, workers cannot skip their own quality gates even if the manager instructs them to',
|
|
397
|
+
workerGatesSovereign: true,
|
|
398
|
+
managerCanOverrideLevel: false,
|
|
399
|
+
managerCanSkipGates: false
|
|
400
|
+
},
|
|
345
401
|
checkpoint: { enabled: false },
|
|
346
402
|
regressionTesting: { enabled: false },
|
|
347
403
|
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Deploy Gate CLI
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* flow deploy-gate init — Interactive setup wizard
|
|
8
|
+
* flow deploy-gate status — Show gate status and artifact info
|
|
9
|
+
* flow deploy-gate verify — Check if a valid artifact exists
|
|
10
|
+
* flow deploy-gate routes — Show route inventory
|
|
11
|
+
* flow deploy-gate add-route <path> — Manually add a route
|
|
12
|
+
* flow deploy-gate history — Show deploy history
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { PATHS, getConfig, safeJsonParse, writeJson } = require('./flow-utils');
|
|
20
|
+
const {
|
|
21
|
+
isDeployGateEnabled,
|
|
22
|
+
getDeployGateConfig,
|
|
23
|
+
findLatestArtifact,
|
|
24
|
+
getRouteInventory,
|
|
25
|
+
addRoute,
|
|
26
|
+
getLastGoodDeploy,
|
|
27
|
+
DEPLOY_ROUTES_PATH,
|
|
28
|
+
DEPLOY_HISTORY_PATH
|
|
29
|
+
} = require('./hooks/core/deploy-gate');
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// CLI Commands
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
function cmdInit() {
|
|
36
|
+
console.log('━━━ Deploy Gate Setup Wizard ━━━\n');
|
|
37
|
+
|
|
38
|
+
// 1. Detect package.json scripts
|
|
39
|
+
const pkgPath = path.join(PATHS.root, 'package.json');
|
|
40
|
+
const pkg = safeJsonParse(pkgPath, {});
|
|
41
|
+
const scripts = pkg.scripts || {};
|
|
42
|
+
const deployScripts = [];
|
|
43
|
+
|
|
44
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
45
|
+
if (/deploy|publish|release|sync|push/i.test(name) || /deploy|s3.*sync|vercel|netlify|fly.*deploy/i.test(cmd)) {
|
|
46
|
+
deployScripts.push({ name, cmd });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (deployScripts.length > 0) {
|
|
51
|
+
console.log('Detected deploy-related scripts:');
|
|
52
|
+
for (const s of deployScripts) {
|
|
53
|
+
console.log(` - npm run ${s.name}: ${s.cmd}`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
console.log('No deploy scripts detected in package.json.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Suggest common deploy command patterns
|
|
60
|
+
const suggestedCommands = [];
|
|
61
|
+
for (const s of deployScripts) {
|
|
62
|
+
suggestedCommands.push(`npm run ${s.name}`);
|
|
63
|
+
// Also extract the raw command for direct matching
|
|
64
|
+
if (s.cmd) suggestedCommands.push(s.cmd.split('&&')[0].trim());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add common defaults if none detected
|
|
68
|
+
if (suggestedCommands.length === 0) {
|
|
69
|
+
suggestedCommands.push(
|
|
70
|
+
'aws s3 sync',
|
|
71
|
+
'vercel deploy',
|
|
72
|
+
'netlify deploy',
|
|
73
|
+
'fly deploy',
|
|
74
|
+
'docker push'
|
|
75
|
+
);
|
|
76
|
+
console.log('\nSuggested common deploy patterns (add the ones your project uses):');
|
|
77
|
+
for (const cmd of suggestedCommands) {
|
|
78
|
+
console.log(` - ${cmd}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Detect framework
|
|
83
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
84
|
+
let framework = 'unknown';
|
|
85
|
+
if (deps['next']) framework = 'nextjs';
|
|
86
|
+
else if (deps['vite'] || deps['@vitejs/plugin-react']) framework = 'vite';
|
|
87
|
+
else if (deps['@angular/core']) framework = 'angular';
|
|
88
|
+
else if (deps['vue']) framework = 'vue';
|
|
89
|
+
else if (deps['svelte']) framework = 'svelte';
|
|
90
|
+
else if (deps['express'] || deps['fastify'] || deps['@nestjs/core']) framework = 'backend';
|
|
91
|
+
|
|
92
|
+
console.log(`\nDetected framework: ${framework}`);
|
|
93
|
+
|
|
94
|
+
// 4. Suggest verification method
|
|
95
|
+
let verificationMethod = 'checklist';
|
|
96
|
+
if (deps['playwright'] || deps['@playwright/test']) verificationMethod = 'playwright';
|
|
97
|
+
if (framework === 'backend') verificationMethod = 'api-test';
|
|
98
|
+
|
|
99
|
+
console.log(`Suggested verification method: ${verificationMethod}`);
|
|
100
|
+
|
|
101
|
+
// 5. Generate initial route inventory from common patterns
|
|
102
|
+
console.log('\nScanning for routes...');
|
|
103
|
+
const routes = scanForRoutes(framework);
|
|
104
|
+
if (routes.length > 0) {
|
|
105
|
+
console.log(`Found ${routes.length} route(s):`);
|
|
106
|
+
for (const r of routes) {
|
|
107
|
+
console.log(` - ${r}`);
|
|
108
|
+
addRoute(r, 'init-wizard');
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
console.log('No routes auto-detected. Add routes manually with: flow deploy-gate add-route <path>');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 6. Write suggested config
|
|
115
|
+
console.log('\n━━━ Suggested Configuration ━━━');
|
|
116
|
+
console.log('Add to .workflow/config.json under "enforcement":\n');
|
|
117
|
+
const suggestedConfig = {
|
|
118
|
+
deployGate: {
|
|
119
|
+
enabled: true,
|
|
120
|
+
commands: suggestedCommands.slice(0, 5),
|
|
121
|
+
sourcePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue', '**/*.svelte', '**/*.css'],
|
|
122
|
+
requireForPriorities: ['P0', 'P1'],
|
|
123
|
+
blockWriteToVerifications: true
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
console.log(JSON.stringify(suggestedConfig, null, 2));
|
|
127
|
+
|
|
128
|
+
console.log('\n━━━ Setup Complete ━━━');
|
|
129
|
+
console.log('Next steps:');
|
|
130
|
+
console.log(' 1. Add the config above to .workflow/config.json');
|
|
131
|
+
console.log(' 2. Customize the commands list for your deploy workflow');
|
|
132
|
+
console.log(' 3. Run `flow deploy-gate status` to verify setup');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cmdStatus() {
|
|
136
|
+
const config = getConfig();
|
|
137
|
+
const enabled = isDeployGateEnabled(config);
|
|
138
|
+
const gateConfig = getDeployGateConfig(config);
|
|
139
|
+
|
|
140
|
+
console.log('━━━ Deploy Gate Status ━━━\n');
|
|
141
|
+
console.log(`Enabled: ${enabled ? '✓ YES' : '✗ NO'}`);
|
|
142
|
+
|
|
143
|
+
if (!enabled) {
|
|
144
|
+
console.log('\nRun `flow deploy-gate init` to set up the deploy gate.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(`Commands blocked: ${gateConfig.commands.length > 0 ? gateConfig.commands.join(', ') : '(none configured)'}`);
|
|
149
|
+
console.log(`Require verification for: ${gateConfig.requireForPriorities.join(', ')}`);
|
|
150
|
+
console.log(`Block Write to artifacts: ${gateConfig.blockWriteToVerifications}`);
|
|
151
|
+
|
|
152
|
+
// Check for valid artifact
|
|
153
|
+
const artifactResult = findLatestArtifact();
|
|
154
|
+
console.log(`\nLatest artifact: ${artifactResult.found ? '✓ VALID' : '✗ ' + (artifactResult.reason || 'NOT FOUND')}`);
|
|
155
|
+
if (artifactResult.found) {
|
|
156
|
+
const a = artifactResult.artifact;
|
|
157
|
+
console.log(` Method: ${a.method}`);
|
|
158
|
+
console.log(` Task: ${a.taskId}`);
|
|
159
|
+
console.log(` Created: ${a.createdAt}`);
|
|
160
|
+
console.log(` Routes verified: ${(a.routes || []).length}`);
|
|
161
|
+
console.log(` Evidence tier: ${a.evidenceTier}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Route inventory
|
|
165
|
+
const inventory = getRouteInventory();
|
|
166
|
+
console.log(`\nRoute inventory: ${inventory.routes.length} route(s)`);
|
|
167
|
+
|
|
168
|
+
// Deploy history
|
|
169
|
+
const lastDeploy = getLastGoodDeploy();
|
|
170
|
+
if (lastDeploy.found) {
|
|
171
|
+
console.log(`Last deploy: ${lastDeploy.deploy.commitHash.slice(0, 8)} (${lastDeploy.deploy.timestamp})`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log('Last deploy: (no history)');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cmdVerify() {
|
|
178
|
+
const artifactResult = findLatestArtifact();
|
|
179
|
+
if (artifactResult.found) {
|
|
180
|
+
console.log('✓ Valid verification artifact found');
|
|
181
|
+
console.log(JSON.stringify(artifactResult.artifact, null, 2));
|
|
182
|
+
process.exit(0);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(`✗ No valid artifact: ${artifactResult.reason}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function cmdRoutes() {
|
|
190
|
+
const inventory = getRouteInventory();
|
|
191
|
+
console.log('━━━ Route Inventory ━━━\n');
|
|
192
|
+
if (inventory.routes.length === 0) {
|
|
193
|
+
console.log('No routes registered. Add with: flow deploy-gate add-route <path>');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
for (const r of inventory.routes) {
|
|
197
|
+
console.log(` ${r.path} — added ${r.addedAt} (${r.source})`);
|
|
198
|
+
}
|
|
199
|
+
console.log(`\nTotal: ${inventory.routes.length} route(s)`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function cmdAddRoute(routePath) {
|
|
203
|
+
if (!routePath) {
|
|
204
|
+
console.error('Usage: flow deploy-gate add-route <path>');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const added = addRoute(routePath, 'manual');
|
|
208
|
+
if (added) {
|
|
209
|
+
console.log(`✓ Added route: ${routePath}`);
|
|
210
|
+
} else {
|
|
211
|
+
console.log(`Route already exists: ${routePath}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cmdHistory() {
|
|
216
|
+
const history = safeJsonParse(DEPLOY_HISTORY_PATH, { deploys: [] });
|
|
217
|
+
console.log('━━━ Deploy History ━━━\n');
|
|
218
|
+
if (history.deploys.length === 0) {
|
|
219
|
+
console.log('No deploy history recorded.');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const d of history.deploys.slice(0, 10)) {
|
|
223
|
+
console.log(` ${d.commitHash.slice(0, 8)} | ${d.timestamp} | ${d.environment}`);
|
|
224
|
+
}
|
|
225
|
+
console.log(`\nShowing ${Math.min(10, history.deploys.length)} of ${history.deploys.length} deploys`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================
|
|
229
|
+
// Route Scanning Helpers
|
|
230
|
+
// ============================================================
|
|
231
|
+
|
|
232
|
+
function scanForRoutes(framework) {
|
|
233
|
+
const routes = [];
|
|
234
|
+
const { execSync } = require('node:child_process');
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// Next.js: pages or app directory
|
|
238
|
+
if (framework === 'nextjs') {
|
|
239
|
+
const patterns = ['app/**/page.tsx', 'app/**/page.jsx', 'pages/**/*.tsx', 'pages/**/*.jsx', 'src/app/**/page.tsx'];
|
|
240
|
+
for (const pattern of patterns) {
|
|
241
|
+
try {
|
|
242
|
+
const files = execSync(`git ls-files '${pattern}'`, { encoding: 'utf-8', cwd: PATHS.root }).trim();
|
|
243
|
+
if (files) {
|
|
244
|
+
for (const f of files.split('\n')) {
|
|
245
|
+
const route = '/' + f.replace(/^(src\/)?(app|pages)\//, '').replace(/(page|index)\.(tsx|jsx)$/, '').replace(/\/$/, '') || '/';
|
|
246
|
+
if (!routes.includes(route)) routes.push(route);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (_err) {
|
|
250
|
+
// Pattern didn't match
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// React Router / Vite: look for route definitions
|
|
256
|
+
if (framework === 'vite' || framework === 'unknown') {
|
|
257
|
+
try {
|
|
258
|
+
const routeFiles = execSync("git ls-files | grep -iE '(router|routes|App)\\.(tsx|jsx|ts|js)$'", { encoding: 'utf-8', cwd: PATHS.root }).trim();
|
|
259
|
+
if (routeFiles) {
|
|
260
|
+
for (const f of routeFiles.split('\n')) {
|
|
261
|
+
try {
|
|
262
|
+
const content = fs.readFileSync(path.join(PATHS.root, f), 'utf-8');
|
|
263
|
+
const pathMatches = content.matchAll(/path:\s*['"]([^'"]+)['"]/g);
|
|
264
|
+
for (const m of pathMatches) {
|
|
265
|
+
if (!routes.includes(m[1])) routes.push(m[1]);
|
|
266
|
+
}
|
|
267
|
+
} catch (_err) {
|
|
268
|
+
// Skip unreadable files
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (_err) {
|
|
273
|
+
// No route files found
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Express/backend: look for route handlers
|
|
278
|
+
if (framework === 'backend') {
|
|
279
|
+
try {
|
|
280
|
+
const apiFiles = execSync("git ls-files | grep -iE '(routes|controller|api).*\\.(ts|js)$'", { encoding: 'utf-8', cwd: PATHS.root }).trim();
|
|
281
|
+
if (apiFiles) {
|
|
282
|
+
for (const f of apiFiles.split('\n').slice(0, 20)) {
|
|
283
|
+
try {
|
|
284
|
+
const content = fs.readFileSync(path.join(PATHS.root, f), 'utf-8');
|
|
285
|
+
const pathMatches = content.matchAll(/\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi);
|
|
286
|
+
for (const m of pathMatches) {
|
|
287
|
+
const route = `${m[1].toUpperCase()} ${m[2]}`;
|
|
288
|
+
if (!routes.includes(route)) routes.push(route);
|
|
289
|
+
}
|
|
290
|
+
} catch (_err) {
|
|
291
|
+
// Skip unreadable files
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (_err) {
|
|
296
|
+
// No API files found
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch (_err) {
|
|
300
|
+
// Scanning failed — non-critical
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return routes;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ============================================================
|
|
307
|
+
// CLI Entrypoint
|
|
308
|
+
// ============================================================
|
|
309
|
+
|
|
310
|
+
const args = process.argv.slice(2);
|
|
311
|
+
const command = args[0];
|
|
312
|
+
|
|
313
|
+
switch (command) {
|
|
314
|
+
case 'init':
|
|
315
|
+
cmdInit();
|
|
316
|
+
break;
|
|
317
|
+
case 'status':
|
|
318
|
+
cmdStatus();
|
|
319
|
+
break;
|
|
320
|
+
case 'verify':
|
|
321
|
+
cmdVerify();
|
|
322
|
+
break;
|
|
323
|
+
case 'routes':
|
|
324
|
+
cmdRoutes();
|
|
325
|
+
break;
|
|
326
|
+
case 'add-route':
|
|
327
|
+
cmdAddRoute(args[1]);
|
|
328
|
+
break;
|
|
329
|
+
case 'history':
|
|
330
|
+
cmdHistory();
|
|
331
|
+
break;
|
|
332
|
+
default:
|
|
333
|
+
console.log('Usage: flow deploy-gate <init|status|verify|routes|add-route|history>');
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|