wogiflow 2.26.2 → 2.29.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/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +149 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-config-loader.js +3 -2
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-correction-detector.js +3 -2
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-prompt-composer.js +3 -2
- package/scripts/flow-prompt-template.js +3 -2
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-mcp-strip.js +122 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- package/.workflow/templates/partials/user-commands.hbs +0 -20
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wogi Flow — Skill Proposal CLI
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* flow skill propose --name <n> --content <file> [--rationale <text>]
|
|
10
|
+
* flow skill patch --name <n> --content <file> [--rationale <text>]
|
|
11
|
+
* flow skill remove --name <n> [--rationale <text>]
|
|
12
|
+
* flow skill promote <name> [--id <proposalId>]
|
|
13
|
+
* flow skill reject <name> [--id <proposalId>]
|
|
14
|
+
* flow skill archive <name>
|
|
15
|
+
* flow skill pending [--json]
|
|
16
|
+
*
|
|
17
|
+
* Writes are staged to .claude/skills/pending/ and .workflow/state/skill-proposals.json.
|
|
18
|
+
* Session-end hook surfaces pending proposals for user review.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const store = require('../lib/skill-proposal-store');
|
|
22
|
+
const { success, error: errorMsg, info, colors } = require('./flow-output');
|
|
23
|
+
|
|
24
|
+
// ============================================================
|
|
25
|
+
// Arg parsing
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
// argv = [subcommand, ...rest]
|
|
30
|
+
const [subcommand, ...rest] = argv;
|
|
31
|
+
const flags = {};
|
|
32
|
+
const positional = [];
|
|
33
|
+
for (let i = 0; i < rest.length; i++) {
|
|
34
|
+
const a = rest[i];
|
|
35
|
+
if (a === '--name') {
|
|
36
|
+
flags.name = rest[++i];
|
|
37
|
+
} else if (a === '--content') {
|
|
38
|
+
flags.content = rest[++i];
|
|
39
|
+
} else if (a === '--find') {
|
|
40
|
+
flags.find = rest[++i];
|
|
41
|
+
} else if (a === '--replace') {
|
|
42
|
+
flags.replace = rest[++i];
|
|
43
|
+
} else if (a === '--rationale') {
|
|
44
|
+
flags.rationale = rest[++i];
|
|
45
|
+
} else if (a === '--id') {
|
|
46
|
+
flags.id = rest[++i];
|
|
47
|
+
} else if (a === '--json') {
|
|
48
|
+
flags.json = true;
|
|
49
|
+
} else if (a === '--proposed-by') {
|
|
50
|
+
flags.proposedBy = rest[++i];
|
|
51
|
+
} else if (a === '--help' || a === '-h') {
|
|
52
|
+
flags.help = true;
|
|
53
|
+
} else if (a && !a.startsWith('--')) {
|
|
54
|
+
positional.push(a);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { subcommand, flags, positional };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function showHelp() {
|
|
61
|
+
console.log(`
|
|
62
|
+
${colors.cyan}Wogi Flow — Skill Proposal CLI${colors.reset}
|
|
63
|
+
|
|
64
|
+
Agent-staged skill changes. Proposals are reviewed at session-end and approved
|
|
65
|
+
or rejected by the user. No auto-apply.
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
flow skill propose --name <n> --content <file> [--rationale <text>]
|
|
69
|
+
flow skill patch --name <n> --content <file> [--rationale <text>]
|
|
70
|
+
flow skill patch --name <n> --find <file> --replace <file> [--rationale <text>]
|
|
71
|
+
flow skill remove --name <n> [--rationale <text>]
|
|
72
|
+
flow skill promote <name> [--id <proposalId>]
|
|
73
|
+
flow skill reject <name> [--id <proposalId>]
|
|
74
|
+
flow skill archive <name>
|
|
75
|
+
flow skill pending [--json]
|
|
76
|
+
|
|
77
|
+
Actions:
|
|
78
|
+
propose Stage a new skill. Writes content to .claude/skills/pending/<n>.md
|
|
79
|
+
patch Stage an edit to an existing skill. Use --content for full
|
|
80
|
+
replacement, or --find + --replace for fuzzy find-replace
|
|
81
|
+
(tolerates whitespace/line-ending drift; rejects low-confidence
|
|
82
|
+
matches below skills.fuzzyPatchThreshold, default 0.85).
|
|
83
|
+
remove Stage a removal of an existing skill.
|
|
84
|
+
promote Apply a pending proposal (user-only). Moves pending → active,
|
|
85
|
+
applies patch, or archives as appropriate.
|
|
86
|
+
reject Discard a pending proposal; cleans up staged content.
|
|
87
|
+
archive Direct archival of an active skill (no staging).
|
|
88
|
+
pending List pending proposals. --json for machine-readable output.
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
flow skill propose --name react-hooks --content scratch/draft.md \\
|
|
92
|
+
--rationale "capture hook patterns from recent session"
|
|
93
|
+
flow skill patch --name react-hooks --content scratch/updated.md
|
|
94
|
+
flow skill remove --name outdated-skill --rationale "superseded by newer skill"
|
|
95
|
+
flow skill promote react-hooks
|
|
96
|
+
flow skill reject react-hooks
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function printPending(list, asJson) {
|
|
101
|
+
if (asJson) {
|
|
102
|
+
process.stdout.write(JSON.stringify(list, null, 2) + '\n');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (list.length === 0) {
|
|
106
|
+
info('No pending skill proposals.');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log(`${colors.cyan}Pending skill proposals (${list.length}):${colors.reset}\n`);
|
|
110
|
+
for (const p of list) {
|
|
111
|
+
const icon = p.action === 'propose' ? '+' : p.action === 'patch' ? '~' : '-';
|
|
112
|
+
console.log(` ${colors.bold}${icon} ${p.skillName}${colors.reset} ${colors.dim}(${p.action}, ${p.id})${colors.reset}`);
|
|
113
|
+
console.log(` proposedAt: ${p.proposedAt} by: ${p.proposedBy}`);
|
|
114
|
+
if (p.contentPath) console.log(` content: ${p.contentPath}`);
|
|
115
|
+
if (p.rationale) console.log(` rationale: ${p.rationale}`);
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================
|
|
121
|
+
// Subcommand handlers
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
function runPropose(flags) {
|
|
125
|
+
const record = store.createProposal({
|
|
126
|
+
action: 'propose',
|
|
127
|
+
skillName: flags.name,
|
|
128
|
+
contentFile: flags.content,
|
|
129
|
+
rationale: flags.rationale,
|
|
130
|
+
proposedBy: flags.proposedBy,
|
|
131
|
+
});
|
|
132
|
+
success(`Staged propose '${record.skillName}' (${record.id})`);
|
|
133
|
+
console.log(` content: ${record.contentPath}`);
|
|
134
|
+
console.log(` review with: flow skill pending`);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function runPatch(flags) {
|
|
139
|
+
const record = store.createProposal({
|
|
140
|
+
action: 'patch',
|
|
141
|
+
skillName: flags.name,
|
|
142
|
+
contentFile: flags.content,
|
|
143
|
+
findFile: flags.find,
|
|
144
|
+
replaceFile: flags.replace,
|
|
145
|
+
rationale: flags.rationale,
|
|
146
|
+
proposedBy: flags.proposedBy,
|
|
147
|
+
});
|
|
148
|
+
success(`Staged patch '${record.skillName}' (${record.id})`);
|
|
149
|
+
if (record.patchMode === 'fuzzy') {
|
|
150
|
+
console.log(` mode: fuzzy find/replace`);
|
|
151
|
+
console.log(` find: ${record.findPath}`);
|
|
152
|
+
console.log(` replace: ${record.replacePath}`);
|
|
153
|
+
} else {
|
|
154
|
+
console.log(` mode: full replacement`);
|
|
155
|
+
console.log(` content: ${record.contentPath}`);
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runRemove(flags) {
|
|
161
|
+
const record = store.createProposal({
|
|
162
|
+
action: 'remove',
|
|
163
|
+
skillName: flags.name,
|
|
164
|
+
rationale: flags.rationale,
|
|
165
|
+
proposedBy: flags.proposedBy,
|
|
166
|
+
});
|
|
167
|
+
success(`Staged remove '${record.skillName}' (${record.id})`);
|
|
168
|
+
console.log(` review with: flow skill pending`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function runPromote(flags, positional) {
|
|
173
|
+
const name = positional[0] || flags.name;
|
|
174
|
+
const id = flags.id;
|
|
175
|
+
if (!name && !id) throw new Error('promote requires a skill name or --id <proposalId>');
|
|
176
|
+
const applied = store.promoteProposal({ skillName: name, id });
|
|
177
|
+
success(`Promoted ${applied.action} '${applied.skillName}' (${applied.id})`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function runReject(flags, positional) {
|
|
182
|
+
const name = positional[0] || flags.name;
|
|
183
|
+
const id = flags.id;
|
|
184
|
+
if (!name && !id) throw new Error('reject requires a skill name or --id <proposalId>');
|
|
185
|
+
const rejected = store.rejectProposal({ skillName: name, id });
|
|
186
|
+
success(`Rejected ${rejected.action} '${rejected.skillName}' (${rejected.id})`);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function runArchive(_flags, positional) {
|
|
191
|
+
const name = positional[0];
|
|
192
|
+
if (!name) throw new Error('archive requires a skill name');
|
|
193
|
+
const r = store.archiveSkill(name);
|
|
194
|
+
success(`Archived '${r.skillName}' → ${r.archivedPath}`);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function runPending(flags) {
|
|
199
|
+
const list = store.listProposals({ status: 'pending' });
|
|
200
|
+
printPending(list, !!flags.json);
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================
|
|
205
|
+
// Main
|
|
206
|
+
// ============================================================
|
|
207
|
+
|
|
208
|
+
function main() {
|
|
209
|
+
const argv = process.argv.slice(2);
|
|
210
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
211
|
+
showHelp();
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { subcommand, flags, positional } = parseArgs(argv);
|
|
216
|
+
|
|
217
|
+
if (flags.help) {
|
|
218
|
+
showHelp();
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
let code;
|
|
224
|
+
switch (subcommand) {
|
|
225
|
+
case 'propose': code = runPropose(flags); break;
|
|
226
|
+
case 'patch': code = runPatch(flags); break;
|
|
227
|
+
case 'remove': code = runRemove(flags); break;
|
|
228
|
+
case 'promote': code = runPromote(flags, positional); break;
|
|
229
|
+
case 'reject': code = runReject(flags, positional); break;
|
|
230
|
+
case 'archive': code = runArchive(flags, positional); break;
|
|
231
|
+
case 'pending': code = runPending(flags); break;
|
|
232
|
+
default:
|
|
233
|
+
errorMsg(`Unknown subcommand: ${subcommand}`);
|
|
234
|
+
showHelp();
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
process.exit(code);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
errorMsg(err.message);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (require.main === module) {
|
|
245
|
+
main();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = { parseArgs, runPropose, runPatch, runRemove, runPromote, runReject, runArchive, runPending };
|
|
@@ -410,7 +410,7 @@ function isLikelyNewFile(content, filePath) {
|
|
|
410
410
|
* @param {string} filePath - File path
|
|
411
411
|
* @returns {string|null} Section name
|
|
412
412
|
*/
|
|
413
|
-
function
|
|
413
|
+
function _findFileSection(content, filePath) {
|
|
414
414
|
const lines = content.split('\n');
|
|
415
415
|
let currentSection = null;
|
|
416
416
|
|
|
@@ -1214,6 +1214,75 @@ Examples:
|
|
|
1214
1214
|
process.exit(results.blocked ? 1 : 0);
|
|
1215
1215
|
}
|
|
1216
1216
|
|
|
1217
|
+
// ============================================================================
|
|
1218
|
+
// Non-Negotiable Rules Validation (wf-d0adca72 / A5)
|
|
1219
|
+
// ============================================================================
|
|
1220
|
+
|
|
1221
|
+
const NON_NEGOTIABLE_FRAGMENT_ID = 'non-negotiable-rules';
|
|
1222
|
+
const NON_NEGOTIABLE_FRAGMENT_PATH = '.workflow/prompts/fragments/non-negotiable-rules.md';
|
|
1223
|
+
const CITATION_FORMAT_REGEX = /\b[\w./-]+\.(?:js|ts|tsx|jsx|md|json|yaml|yml|py|go|rs|sh):\d+(?:-\d+)?\b/;
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Validate that the non-negotiable-rules fragment exists and is loadable.
|
|
1227
|
+
* @returns {{ ok: boolean, reason?: string, path?: string }}
|
|
1228
|
+
*/
|
|
1229
|
+
function checkNonNegotiableFragment() {
|
|
1230
|
+
const fs = require('node:fs');
|
|
1231
|
+
const path = require('node:path');
|
|
1232
|
+
const full = path.join(process.cwd(), NON_NEGOTIABLE_FRAGMENT_PATH);
|
|
1233
|
+
if (!fs.existsSync(full)) {
|
|
1234
|
+
return { ok: false, reason: `missing fragment: ${NON_NEGOTIABLE_FRAGMENT_PATH}`, path: full };
|
|
1235
|
+
}
|
|
1236
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
1237
|
+
const required = ['Evidence before claim', 'No silent scope changes', 'Route every request', 'filepath:line', 'Destructive operations', 'Do not invent artifacts'];
|
|
1238
|
+
const missing = required.filter((r) => !content.includes(r));
|
|
1239
|
+
if (missing.length > 0) {
|
|
1240
|
+
return { ok: false, reason: `fragment missing required sections: ${missing.join(', ')}`, path: full };
|
|
1241
|
+
}
|
|
1242
|
+
return { ok: true, path: full };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Validate that a composed prompt includes the non-negotiable-rules block.
|
|
1247
|
+
* @param {string} composedPrompt
|
|
1248
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
1249
|
+
*/
|
|
1250
|
+
function checkComposedPromptHasNonNegotiables(composedPrompt) {
|
|
1251
|
+
if (!composedPrompt || typeof composedPrompt !== 'string') {
|
|
1252
|
+
return { ok: false, reason: 'composedPrompt must be a non-empty string' };
|
|
1253
|
+
}
|
|
1254
|
+
if (!composedPrompt.includes('Non-Negotiable Rules')) {
|
|
1255
|
+
return { ok: false, reason: 'composed prompt missing "Non-Negotiable Rules" header — fragment not loaded' };
|
|
1256
|
+
}
|
|
1257
|
+
if (!composedPrompt.includes('filepath:line')) {
|
|
1258
|
+
return { ok: false, reason: 'composed prompt missing citation-format rule' };
|
|
1259
|
+
}
|
|
1260
|
+
return { ok: true };
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Validate that a text body contains at least one filepath:line citation when it makes claims about code.
|
|
1265
|
+
* Heuristic: if the text references code (function names with parens, paths with slashes, or quoted identifiers),
|
|
1266
|
+
* it should cite at least one location in `path:line` format.
|
|
1267
|
+
* @param {string} text
|
|
1268
|
+
* @param {object} [opts]
|
|
1269
|
+
* @param {boolean} [opts.requireCitation=true]
|
|
1270
|
+
* @returns {{ ok: boolean, reason?: string, hasCitation: boolean }}
|
|
1271
|
+
*/
|
|
1272
|
+
function checkCitationFormat(text, { requireCitation = true } = {}) {
|
|
1273
|
+
if (!text || typeof text !== 'string') {
|
|
1274
|
+
return { ok: false, reason: 'text must be a non-empty string', hasCitation: false };
|
|
1275
|
+
}
|
|
1276
|
+
const hasCitation = CITATION_FORMAT_REGEX.test(text);
|
|
1277
|
+
if (!requireCitation) return { ok: true, hasCitation };
|
|
1278
|
+
// Make claim-about-code detection lightweight
|
|
1279
|
+
const looksLikeCodeClaim = /\b(function|class|module|import|require|file|path)\b|`[^`]+`/.test(text);
|
|
1280
|
+
if (looksLikeCodeClaim && !hasCitation) {
|
|
1281
|
+
return { ok: false, reason: 'text references code but has no filepath:line citation', hasCitation: false };
|
|
1282
|
+
}
|
|
1283
|
+
return { ok: true, hasCitation };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1217
1286
|
// ============================================================================
|
|
1218
1287
|
// Exports
|
|
1219
1288
|
// ============================================================================
|
|
@@ -1221,6 +1290,12 @@ Examples:
|
|
|
1221
1290
|
module.exports = {
|
|
1222
1291
|
runStandardsCheck,
|
|
1223
1292
|
formatStandardsResults,
|
|
1293
|
+
checkNonNegotiableFragment,
|
|
1294
|
+
checkComposedPromptHasNonNegotiables,
|
|
1295
|
+
checkCitationFormat,
|
|
1296
|
+
NON_NEGOTIABLE_FRAGMENT_ID,
|
|
1297
|
+
NON_NEGOTIABLE_FRAGMENT_PATH,
|
|
1298
|
+
CITATION_FORMAT_REGEX,
|
|
1224
1299
|
parseDecisions,
|
|
1225
1300
|
parseAppMap,
|
|
1226
1301
|
parseFunctionMap,
|
|
@@ -42,6 +42,11 @@ const FORMATS = {
|
|
|
42
42
|
name: 'Detailed',
|
|
43
43
|
description: 'Full info including skill and worktree',
|
|
44
44
|
format: '{{#if workspace.git_worktree}}[WT] {{/if}}{{#if task}}[{{task.id}}] {{task.title}} | {{/if}}{{model}} | {{context_window.used_percentage}}% used{{#if skill}} | {{skill}}{{/if}}'
|
|
45
|
+
},
|
|
46
|
+
advanced: {
|
|
47
|
+
name: 'Advanced',
|
|
48
|
+
description: 'Detailed + effort level and thinking state (Claude Code 2.1.119+)',
|
|
49
|
+
format: '{{#if workspace.git_worktree}}[WT] {{/if}}{{#if task}}[{{task.id}}] {{task.title}} | {{/if}}{{model}} | {{context_window.used_percentage}}%{{#if effort.level}} | {{effort.level}}{{/if}}{{#if thinking.enabled}} | thinking{{/if}}{{#if skill}} | {{skill}}{{/if}}'
|
|
45
50
|
}
|
|
46
51
|
};
|
|
47
52
|
|
|
@@ -157,7 +162,7 @@ async function interactiveSetup() {
|
|
|
157
162
|
showCurrentConfig();
|
|
158
163
|
showFormats();
|
|
159
164
|
|
|
160
|
-
const format = await question(`\nChoose format (minimal/compact/standard/detailed) [standard]: `);
|
|
165
|
+
const format = await question(`\nChoose format (minimal/compact/standard/detailed/advanced) [standard]: `);
|
|
161
166
|
const selectedFormat = format.trim() || 'standard';
|
|
162
167
|
|
|
163
168
|
if (!FORMATS[selectedFormat]) {
|
|
@@ -226,6 +231,7 @@ Formats:
|
|
|
226
231
|
compact - Task ID + model + context %
|
|
227
232
|
standard - Task ID + model + labeled context (recommended)
|
|
228
233
|
detailed - Worktree + task + model + context % + skill
|
|
234
|
+
advanced - Detailed + effort level + thinking state (Claude Code 2.1.119+)
|
|
229
235
|
|
|
230
236
|
Refresh interval (Claude Code 2.1.97+):
|
|
231
237
|
Re-runs the status line every N seconds so live values like task ID,
|
|
@@ -281,7 +287,7 @@ Examples:
|
|
|
281
287
|
if (formatIndex >= 0) {
|
|
282
288
|
const format = args[formatIndex + 1];
|
|
283
289
|
if (!format || !FORMATS[format]) {
|
|
284
|
-
errorMsg('Invalid format. Use: minimal, compact, standard, or
|
|
290
|
+
errorMsg('Invalid format. Use: minimal, compact, standard, detailed, or advanced');
|
|
285
291
|
process.exit(1);
|
|
286
292
|
}
|
|
287
293
|
|
|
@@ -26,7 +26,7 @@ const CHANGELOG_PATH = path.join(PATHS.root, 'CHANGELOG.md');
|
|
|
26
26
|
* @returns {object} - { passed: boolean, message: string, entry?: string }
|
|
27
27
|
*/
|
|
28
28
|
async function run(options = {}) {
|
|
29
|
-
const { taskId, taskTitle, taskType, files = [], mode,
|
|
29
|
+
const { taskId, taskTitle, taskType, files = [], mode, _stepConfig = {} } = options;
|
|
30
30
|
|
|
31
31
|
// Determine changelog category
|
|
32
32
|
const category = getChangelogCategory(taskType, taskTitle, files);
|
|
@@ -133,7 +133,7 @@ function getChangelogCategory(taskType, taskTitle, _files) {
|
|
|
133
133
|
/**
|
|
134
134
|
* Generate a changelog entry
|
|
135
135
|
*/
|
|
136
|
-
function generateEntry(taskId, taskTitle, _category,
|
|
136
|
+
function generateEntry(taskId, taskTitle, _category, _files) {
|
|
137
137
|
// Clean up title
|
|
138
138
|
let entry = taskTitle || 'Update';
|
|
139
139
|
|
|
@@ -31,7 +31,7 @@ const COVERAGE_PATHS = [
|
|
|
31
31
|
* @returns {object} - { passed: boolean, message: string, coverage?: object }
|
|
32
32
|
*/
|
|
33
33
|
async function run(options = {}) {
|
|
34
|
-
const { files = [], stepConfig = {},
|
|
34
|
+
const { files = [], stepConfig = {}, _mode } = options;
|
|
35
35
|
const minCoverage = stepConfig.minCoverage || 80;
|
|
36
36
|
const checkFiles = stepConfig.checkModifiedOnly ?? true;
|
|
37
37
|
|
|
@@ -26,7 +26,7 @@ const KNOWLEDGE_DIR = path.join(PATHS.root, '.claude', 'docs', 'knowledge-base')
|
|
|
26
26
|
* @returns {object} - { passed: boolean, message: string, suggestion?: string }
|
|
27
27
|
*/
|
|
28
28
|
async function run(options = {}) {
|
|
29
|
-
const {
|
|
29
|
+
const { _taskId, taskTitle, files = [], mode, _stepConfig = {}, learnings } = options;
|
|
30
30
|
|
|
31
31
|
// Ensure knowledge base directory exists
|
|
32
32
|
if (!fs.existsSync(KNOWLEDGE_DIR)) {
|
|
@@ -21,7 +21,7 @@ const { PATHS } = require('./flow-utils');
|
|
|
21
21
|
* @returns {object} - { passed: boolean, message: string, details?: object }
|
|
22
22
|
*/
|
|
23
23
|
async function run(options = {}) {
|
|
24
|
-
const { stepConfig = {},
|
|
24
|
+
const { stepConfig = {}, _mode } = options;
|
|
25
25
|
const sampleSize = stepConfig.sampleSize || 3;
|
|
26
26
|
|
|
27
27
|
try {
|
|
@@ -308,7 +308,7 @@ function findDuplicationPatterns(content, fileName) {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
// Report lines that appear 3+ times
|
|
311
|
-
for (const [
|
|
311
|
+
for (const [_pattern, lineNumbers] of Object.entries(patterns)) {
|
|
312
312
|
if (lineNumbers.length >= 3) {
|
|
313
313
|
suggestions.push({
|
|
314
314
|
file: fileName,
|
|
@@ -355,7 +355,7 @@ function determineCapabilities(text, complexity) {
|
|
|
355
355
|
* @returns {Object} Token estimates
|
|
356
356
|
*/
|
|
357
357
|
function estimateTaskTokens(analysis) {
|
|
358
|
-
const { complexity, domains,
|
|
358
|
+
const { complexity, domains, _languages } = analysis;
|
|
359
359
|
|
|
360
360
|
const multiplier = TOKEN_FACTORS.COMPLEXITY_MULTIPLIER[complexity.level];
|
|
361
361
|
const baseInput = TOKEN_FACTORS.BASE_INPUT;
|
|
@@ -176,7 +176,7 @@ function classifyTask(taskDescription, affectedFiles = [], _options = {}) {
|
|
|
176
176
|
.sort((a, b) => b[1] - a[1]);
|
|
177
177
|
|
|
178
178
|
const [topType, topScore] = sortedTypes[0];
|
|
179
|
-
const [
|
|
179
|
+
const [_secondType, secondScore] = sortedTypes[1] || ['none', 0];
|
|
180
180
|
|
|
181
181
|
// Calculate confidence
|
|
182
182
|
let confidence = 'high';
|
|
@@ -752,7 +752,7 @@ function getLoopStats() {
|
|
|
752
752
|
*/
|
|
753
753
|
function verifyCriterion(criterion, context = {}) {
|
|
754
754
|
const { execSync, execFileSync } = require('node:child_process');
|
|
755
|
-
const {
|
|
755
|
+
const { _changedFiles = [], testResults = null, lintResults = null } = context;
|
|
756
756
|
const _config = getConfig();
|
|
757
757
|
const taskConfig = getTaskConfig();
|
|
758
758
|
const desc = criterion.description;
|
|
@@ -461,7 +461,7 @@ function getIndent(line) {
|
|
|
461
461
|
async function extractTemplates(projectRoot, options = {}) {
|
|
462
462
|
const {
|
|
463
463
|
types = Object.keys(FILE_TYPES),
|
|
464
|
-
|
|
464
|
+
_outputDir = path.join(projectRoot, '.workflow', 'templates', 'extracted')
|
|
465
465
|
} = options;
|
|
466
466
|
|
|
467
467
|
const startTime = Date.now();
|
|
@@ -264,7 +264,7 @@ function parseTypeScript(file) {
|
|
|
264
264
|
/(?:^|\n)\s*(?:export\s+)?(interface|class|type)\s+([A-Z]\w*)\b\s*(?:extends[^{]*|implements[^{]*|<[^>]*>\s*)?(?:=\s*)?(\{)/g;
|
|
265
265
|
let m;
|
|
266
266
|
while ((m = headerRegex.exec(content)) !== null) {
|
|
267
|
-
const [, kind, name,
|
|
267
|
+
const [, kind, name, _openBrace] = m;
|
|
268
268
|
const bodyStart = m.index + m[0].length;
|
|
269
269
|
const body = extractBalancedBlock(content, bodyStart - 1); // include the opening brace
|
|
270
270
|
if (!body) continue;
|
package/scripts/flow-utils.js
CHANGED
|
@@ -262,6 +262,10 @@ function isValidWogiId(id) {
|
|
|
262
262
|
if (/^wf-rv-[a-f0-9]{8}$/i.test(id)) return true;
|
|
263
263
|
// Epic, feature, plan IDs
|
|
264
264
|
if (/^(ep|ft|pl)-[a-f0-9]{8}$/i.test(id)) return true;
|
|
265
|
+
// Slug format: wf-<alphanum>[<alphanum or hyphen>]*<alphanum>, 5-64 chars.
|
|
266
|
+
// For manager-dispatched descriptive IDs. Path-safe (no dots/separators).
|
|
267
|
+
// Keep this in sync with validateTaskId() 'slug' branch in flow-id.js.
|
|
268
|
+
if (/^wf-[a-z0-9][a-z0-9-]{0,60}[a-z0-9]$/i.test(id)) return true;
|
|
265
269
|
// Legacy format
|
|
266
270
|
if (/^(TASK|BUG)-\d{3,}$/i.test(id)) return true;
|
|
267
271
|
return false;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Worker MCP Strip Helper
|
|
5
|
+
*
|
|
6
|
+
* Generates a channel-only MCP config for worker boot. This is the proper
|
|
7
|
+
* fix for the audit-channel-transport-001 regression: Story A originally
|
|
8
|
+
* wrote `{"mcpServers":{}}` (fully empty) for boot speed, which silently
|
|
9
|
+
* stripped the `wogi-workspace-channel` MCP server — leaving manager-side
|
|
10
|
+
* `workspace_send_message` HTTP-POSTs unable to reach the worker.
|
|
11
|
+
*
|
|
12
|
+
* This script reads the worker member-repo's real `.mcp.json`, extracts
|
|
13
|
+
* ONLY the `wogi-workspace-channel` entry, and writes a channel-only
|
|
14
|
+
* config to a destination path. Result:
|
|
15
|
+
* - claude.ai MCP integrations remain stripped (Story A's boot-speed win)
|
|
16
|
+
* - The workspace transport remains active (manager dispatch works)
|
|
17
|
+
*
|
|
18
|
+
* Fallback: if the source `.mcp.json` doesn't define
|
|
19
|
+
* `wogi-workspace-channel` (e.g. the worker isn't a workspace member),
|
|
20
|
+
* the destination is written with `{"mcpServers":{}}` — harmless in
|
|
21
|
+
* non-workspace contexts.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* node flow-worker-mcp-strip.js <source-mcp.json> <dest-mcp.json>
|
|
25
|
+
*
|
|
26
|
+
* Programmatic:
|
|
27
|
+
* const { extractChannelOnlyConfig, writeChannelOnlyConfig } =
|
|
28
|
+
* require('./flow-worker-mcp-strip');
|
|
29
|
+
* const cfg = extractChannelOnlyConfig(srcPath);
|
|
30
|
+
* writeChannelOnlyConfig(destPath, cfg);
|
|
31
|
+
*
|
|
32
|
+
* Exit codes:
|
|
33
|
+
* 0 — success (channel-only or empty config written)
|
|
34
|
+
* 1 — write failure (caller should fall back to no-strip)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const fs = require('node:fs');
|
|
40
|
+
const path = require('node:path');
|
|
41
|
+
|
|
42
|
+
const CHANNEL_SERVER_NAME = 'wogi-workspace-channel';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read the source `.mcp.json` and return the channel-only config object.
|
|
46
|
+
* Never throws; returns the empty-config fallback on any failure.
|
|
47
|
+
*/
|
|
48
|
+
function extractChannelOnlyConfig(sourcePath) {
|
|
49
|
+
const empty = { mcpServers: {} };
|
|
50
|
+
if (!sourcePath || typeof sourcePath !== 'string') return empty;
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(sourcePath)) return empty;
|
|
53
|
+
const raw = fs.readFileSync(sourcePath, 'utf-8');
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.mcpServers) return empty;
|
|
56
|
+
const entry = parsed.mcpServers[CHANNEL_SERVER_NAME];
|
|
57
|
+
if (!entry || typeof entry !== 'object') return empty;
|
|
58
|
+
return { mcpServers: { [CHANNEL_SERVER_NAME]: entry } };
|
|
59
|
+
} catch (_err) {
|
|
60
|
+
return empty;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Atomically write the channel-only config to destPath. Returns true on
|
|
66
|
+
* success, false on failure (caller should fall back).
|
|
67
|
+
*/
|
|
68
|
+
function writeChannelOnlyConfig(destPath, config) {
|
|
69
|
+
if (!destPath || typeof destPath !== 'string') return false;
|
|
70
|
+
try {
|
|
71
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
72
|
+
const tmp = `${destPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
73
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n');
|
|
74
|
+
fs.renameSync(tmp, destPath);
|
|
75
|
+
return true;
|
|
76
|
+
} catch (_err) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether the resulting config preserves the channel transport (i.e. the
|
|
83
|
+
* worker will be reachable from the manager). Useful for callers that want
|
|
84
|
+
* to log a warning if dispatch will silently fail.
|
|
85
|
+
*/
|
|
86
|
+
function preservesChannelTransport(config) {
|
|
87
|
+
return Boolean(
|
|
88
|
+
config &&
|
|
89
|
+
config.mcpServers &&
|
|
90
|
+
config.mcpServers[CHANNEL_SERVER_NAME] &&
|
|
91
|
+
typeof config.mcpServers[CHANNEL_SERVER_NAME] === 'object'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
CHANNEL_SERVER_NAME,
|
|
97
|
+
extractChannelOnlyConfig,
|
|
98
|
+
writeChannelOnlyConfig,
|
|
99
|
+
preservesChannelTransport
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (require.main === module) {
|
|
103
|
+
const [src, dest] = process.argv.slice(2);
|
|
104
|
+
if (!src || !dest) {
|
|
105
|
+
process.stderr.write('Usage: flow-worker-mcp-strip <source-mcp.json> <dest-mcp.json>\n');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const cfg = extractChannelOnlyConfig(src);
|
|
109
|
+
const ok = writeChannelOnlyConfig(dest, cfg);
|
|
110
|
+
if (!ok) {
|
|
111
|
+
process.stderr.write(`[flow-worker-mcp-strip] failed to write ${dest}\n`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
if (!preservesChannelTransport(cfg)) {
|
|
115
|
+
process.stderr.write(
|
|
116
|
+
`[flow-worker-mcp-strip] WARNING: ${src} did not define ${CHANNEL_SERVER_NAME} — ` +
|
|
117
|
+
`worker will boot but manager dispatch will fail. ` +
|
|
118
|
+
`Run "flow workspace init" in the workspace root to regenerate .mcp.json.\n`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|