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.
Files changed (169) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +149 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-config-loader.js +3 -2
  70. package/scripts/flow-context-compact/expander.js +1 -1
  71. package/scripts/flow-context-compact/section-extractor.js +2 -2
  72. package/scripts/flow-context-gatherer.js +1 -1
  73. package/scripts/flow-context-generator.js +1 -1
  74. package/scripts/flow-context-scoring.js +1 -1
  75. package/scripts/flow-correct.js +1 -1
  76. package/scripts/flow-correction-detector.js +3 -2
  77. package/scripts/flow-decision-authority.js +66 -15
  78. package/scripts/flow-done.js +33 -1
  79. package/scripts/flow-epic-cascade.js +171 -0
  80. package/scripts/flow-epics.js +2 -7
  81. package/scripts/flow-eval-judge.js +1 -1
  82. package/scripts/flow-eval.js +1 -1
  83. package/scripts/flow-export-scanner.js +2 -6
  84. package/scripts/flow-failure-learning.js +1 -1
  85. package/scripts/flow-feature-dossier.js +787 -0
  86. package/scripts/flow-figma-extract.js +2 -2
  87. package/scripts/flow-figma-generate.js +1 -1
  88. package/scripts/flow-gate-confidence.js +1 -1
  89. package/scripts/flow-health.js +52 -1
  90. package/scripts/flow-hooks.js +1 -1
  91. package/scripts/flow-id.js +19 -3
  92. package/scripts/flow-instruction-richness.js +1 -1
  93. package/scripts/flow-knowledge-router.js +1 -1
  94. package/scripts/flow-knowledge-sync.js +1 -1
  95. package/scripts/flow-logic-adversary.js +76 -1
  96. package/scripts/flow-logic-rules.js +380 -0
  97. package/scripts/flow-long-input.js +5 -5
  98. package/scripts/flow-memory-sync.js +1 -1
  99. package/scripts/flow-memory.js +78 -7
  100. package/scripts/flow-migrate.js +1 -1
  101. package/scripts/flow-model-caller.js +1 -1
  102. package/scripts/flow-models.js +2 -2
  103. package/scripts/flow-morning.js +0 -17
  104. package/scripts/flow-multi-approach.js +1 -1
  105. package/scripts/flow-orchestrate-context.js +4 -4
  106. package/scripts/flow-orchestrate-templates.js +1 -1
  107. package/scripts/flow-orchestrate.js +8 -8
  108. package/scripts/flow-peer-review.js +1 -1
  109. package/scripts/flow-phase.js +9 -0
  110. package/scripts/flow-proactive-compact.js +1 -1
  111. package/scripts/flow-prompt-composer.js +3 -2
  112. package/scripts/flow-prompt-template.js +3 -2
  113. package/scripts/flow-providers.js +1 -1
  114. package/scripts/flow-question-queue.js +255 -0
  115. package/scripts/flow-repo-map.js +312 -0
  116. package/scripts/flow-review-passes/index.js +1 -1
  117. package/scripts/flow-review-passes/integration.js +1 -1
  118. package/scripts/flow-review-passes/structure.js +1 -1
  119. package/scripts/flow-revision-tracker.js +1 -1
  120. package/scripts/flow-section-resolver.js +1 -1
  121. package/scripts/flow-session-end.js +74 -5
  122. package/scripts/flow-session-state.js +103 -1
  123. package/scripts/flow-setup-hooks.js +1 -1
  124. package/scripts/flow-skeptical-evaluator.js +274 -0
  125. package/scripts/flow-skill-generator.js +3 -3
  126. package/scripts/flow-skill-learn.js +3 -6
  127. package/scripts/flow-skill-manage.js +248 -0
  128. package/scripts/flow-spec-verifier.js +1 -1
  129. package/scripts/flow-standards-checker.js +75 -0
  130. package/scripts/flow-standards-gate.js +1 -1
  131. package/scripts/flow-statusline-setup.js +8 -2
  132. package/scripts/flow-step-changelog.js +2 -2
  133. package/scripts/flow-step-coverage.js +1 -1
  134. package/scripts/flow-step-knowledge.js +1 -1
  135. package/scripts/flow-step-regression.js +1 -1
  136. package/scripts/flow-step-simplifier.js +1 -1
  137. package/scripts/flow-task-analyzer.js +1 -1
  138. package/scripts/flow-task-classifier.js +1 -1
  139. package/scripts/flow-task-enforcer.js +1 -1
  140. package/scripts/flow-template-extractor.js +1 -1
  141. package/scripts/flow-trap-zone.js +1 -1
  142. package/scripts/flow-utils.js +4 -0
  143. package/scripts/flow-worker-mcp-strip.js +122 -0
  144. package/scripts/flow-worker-question-classifier.js +51 -5
  145. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  146. package/scripts/flow-workspace-summary.js +256 -0
  147. package/scripts/hooks/adapters/base-adapter.js +2 -2
  148. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  149. package/scripts/hooks/core/observation-capture.js +24 -0
  150. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  151. package/scripts/hooks/core/phase-gate.js +15 -1
  152. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  153. package/scripts/hooks/core/post-compact.js +5 -2
  154. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  155. package/scripts/hooks/core/routing-gate.js +58 -0
  156. package/scripts/hooks/core/session-context.js +108 -0
  157. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  158. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  159. package/scripts/hooks/core/session-end.js +25 -0
  160. package/scripts/hooks/core/setup-handler.js +1 -1
  161. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  162. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  163. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  164. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  165. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  166. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  167. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  168. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  169. 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 findFileSection(content, filePath) {
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,
@@ -23,7 +23,7 @@ const {
23
23
  runStandardsCheck,
24
24
  formatStandardsResults,
25
25
  collectReuseCandidates,
26
- TASK_CHECK_MAP
26
+ _TASK_CHECK_MAP
27
27
  } = require('./flow-standards-checker');
28
28
 
29
29
  // Gate telemetry — records every invocation for self-assessment.
@@ -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 detailed');
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, stepConfig = {} } = options;
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, files) {
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 = {}, mode } = options;
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 { taskId, taskTitle, files = [], mode, stepConfig = {}, learnings } = options;
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 = {}, mode } = options;
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 [pattern, lineNumbers] of Object.entries(patterns)) {
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, languages } = analysis;
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 [secondType, secondScore] = sortedTypes[1] || ['none', 0];
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 { changedFiles = [], testResults = null, lintResults = null } = context;
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
- outputDir = path.join(projectRoot, '.workflow', 'templates', 'extracted')
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, openBrace] = m;
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;
@@ -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
+ }