wogiflow 2.26.2 → 2.29.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.
Files changed (164) 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 +84 -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-context-compact/expander.js +1 -1
  70. package/scripts/flow-context-compact/section-extractor.js +2 -2
  71. package/scripts/flow-context-gatherer.js +1 -1
  72. package/scripts/flow-context-generator.js +1 -1
  73. package/scripts/flow-context-scoring.js +1 -1
  74. package/scripts/flow-correct.js +1 -1
  75. package/scripts/flow-decision-authority.js +66 -15
  76. package/scripts/flow-done.js +33 -1
  77. package/scripts/flow-epic-cascade.js +171 -0
  78. package/scripts/flow-epics.js +2 -7
  79. package/scripts/flow-eval-judge.js +1 -1
  80. package/scripts/flow-eval.js +1 -1
  81. package/scripts/flow-export-scanner.js +2 -6
  82. package/scripts/flow-failure-learning.js +1 -1
  83. package/scripts/flow-feature-dossier.js +787 -0
  84. package/scripts/flow-figma-extract.js +2 -2
  85. package/scripts/flow-figma-generate.js +1 -1
  86. package/scripts/flow-gate-confidence.js +1 -1
  87. package/scripts/flow-health.js +52 -1
  88. package/scripts/flow-hooks.js +1 -1
  89. package/scripts/flow-id.js +19 -3
  90. package/scripts/flow-instruction-richness.js +1 -1
  91. package/scripts/flow-knowledge-router.js +1 -1
  92. package/scripts/flow-knowledge-sync.js +1 -1
  93. package/scripts/flow-logic-adversary.js +76 -1
  94. package/scripts/flow-logic-rules.js +380 -0
  95. package/scripts/flow-long-input.js +5 -5
  96. package/scripts/flow-memory-sync.js +1 -1
  97. package/scripts/flow-memory.js +78 -7
  98. package/scripts/flow-migrate.js +1 -1
  99. package/scripts/flow-model-caller.js +1 -1
  100. package/scripts/flow-models.js +2 -2
  101. package/scripts/flow-morning.js +0 -17
  102. package/scripts/flow-multi-approach.js +1 -1
  103. package/scripts/flow-orchestrate-context.js +4 -4
  104. package/scripts/flow-orchestrate-templates.js +1 -1
  105. package/scripts/flow-orchestrate.js +8 -8
  106. package/scripts/flow-peer-review.js +1 -1
  107. package/scripts/flow-phase.js +9 -0
  108. package/scripts/flow-proactive-compact.js +1 -1
  109. package/scripts/flow-providers.js +1 -1
  110. package/scripts/flow-question-queue.js +255 -0
  111. package/scripts/flow-repo-map.js +312 -0
  112. package/scripts/flow-review-passes/index.js +1 -1
  113. package/scripts/flow-review-passes/integration.js +1 -1
  114. package/scripts/flow-review-passes/structure.js +1 -1
  115. package/scripts/flow-revision-tracker.js +1 -1
  116. package/scripts/flow-section-resolver.js +1 -1
  117. package/scripts/flow-session-end.js +74 -5
  118. package/scripts/flow-session-state.js +103 -1
  119. package/scripts/flow-setup-hooks.js +1 -1
  120. package/scripts/flow-skeptical-evaluator.js +274 -0
  121. package/scripts/flow-skill-generator.js +3 -3
  122. package/scripts/flow-skill-learn.js +3 -6
  123. package/scripts/flow-skill-manage.js +248 -0
  124. package/scripts/flow-spec-verifier.js +1 -1
  125. package/scripts/flow-standards-checker.js +75 -0
  126. package/scripts/flow-standards-gate.js +1 -1
  127. package/scripts/flow-statusline-setup.js +8 -2
  128. package/scripts/flow-step-changelog.js +2 -2
  129. package/scripts/flow-step-coverage.js +1 -1
  130. package/scripts/flow-step-knowledge.js +1 -1
  131. package/scripts/flow-step-regression.js +1 -1
  132. package/scripts/flow-step-simplifier.js +1 -1
  133. package/scripts/flow-task-analyzer.js +1 -1
  134. package/scripts/flow-task-classifier.js +1 -1
  135. package/scripts/flow-task-enforcer.js +1 -1
  136. package/scripts/flow-template-extractor.js +1 -1
  137. package/scripts/flow-trap-zone.js +1 -1
  138. package/scripts/flow-utils.js +4 -0
  139. package/scripts/flow-worker-question-classifier.js +51 -5
  140. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  141. package/scripts/flow-workspace-summary.js +256 -0
  142. package/scripts/hooks/adapters/base-adapter.js +2 -2
  143. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  144. package/scripts/hooks/core/observation-capture.js +24 -0
  145. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  146. package/scripts/hooks/core/phase-gate.js +15 -1
  147. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  148. package/scripts/hooks/core/post-compact.js +5 -2
  149. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  150. package/scripts/hooks/core/routing-gate.js +58 -0
  151. package/scripts/hooks/core/session-context.js +108 -0
  152. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  153. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  154. package/scripts/hooks/core/session-end.js +25 -0
  155. package/scripts/hooks/core/setup-handler.js +1 -1
  156. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  157. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  158. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  159. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  160. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  161. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  162. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  163. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  164. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Skeptical Evaluator (wf-15175dbc / B5).
5
+ *
6
+ * Validating-phase agent that reads the spec + delivery, then forces the AI
7
+ * through a field-by-field enumeration before letting a "done" claim stand.
8
+ *
9
+ * The evaluator composes a prompt with three enumeration passes:
10
+ * 1. UI-field enumeration — for every modified UI surface, list every
11
+ * input/select/textarea/custom field. Reuses the B3 template schema.
12
+ * 2. API-parameter enumeration — for every touched endpoint, list every
13
+ * request/response field.
14
+ * 3. State-key enumeration — for every touched state file or config key,
15
+ * list every entry.
16
+ *
17
+ * For each enumerated item, the evaluator demands a Tier classification
18
+ * (reuse flow-completion-truth-gate EVIDENCE_TIERS) and a confidencePct
19
+ * per the 95/85/75 rubric (wf-f14dcfeb / A4).
20
+ *
21
+ * Composition flow (orchestrator calls this, then invokes Agent tool with the prompt):
22
+ * const { buildSkepticalPrompt, parseSkepticalOutput } = require('./flow-skeptical-evaluator');
23
+ * const prompt = buildSkepticalPrompt({ specMarkdown, diffText, changedFiles, taskId });
24
+ * // ... orchestrator invokes Agent tool with prompt ...
25
+ * const result = parseSkepticalOutput(agentResponse, { taskId });
26
+ *
27
+ * Story: wf-15175dbc (B5)
28
+ * Epic: wf-34290000
29
+ */
30
+
31
+ const path = require('node:path');
32
+
33
+ const { PATHS } = require('./flow-paths');
34
+ const { getConfig } = require('./flow-config-loader');
35
+ const {
36
+ parseBELItems,
37
+ extractSpecStrings,
38
+ verifyBELAgainstDelivery,
39
+ verifySpecBundleCoverage,
40
+ } = require('./flow-completion-truth-gate');
41
+
42
+ const TEMPLATE_PATH = path.join(PATHS.workflow, 'templates', 'tier3-dom-field-inventory.md');
43
+
44
+ function _isDisabled() {
45
+ const cfg = getConfig();
46
+ const igr = cfg.intentGroundedReasoning || {};
47
+ if (igr.enabled === false) return { disabled: true, reason: 'igr-disabled' };
48
+ const se = igr.skepticalEvaluator || {};
49
+ if (se.enabled === false) return { disabled: true, reason: 'skeptical-evaluator-disabled' };
50
+ return { disabled: false };
51
+ }
52
+
53
+ /**
54
+ * Build the system + user prompt for the skeptical evaluator sub-agent.
55
+ *
56
+ * @param {object} opts
57
+ * @param {string} opts.specMarkdown - spec file content
58
+ * @param {string} opts.diffText - git diff
59
+ * @param {string[]} [opts.changedFiles]
60
+ * @param {string} [opts.commitMessage]
61
+ * @param {string} [opts.taskId]
62
+ * @param {string} [opts.bundleText] - built-bundle text if available
63
+ * @returns {{ systemPrompt: string, userPrompt: string, preChecks: object, metadata: object }}
64
+ */
65
+ function buildSkepticalPrompt(opts) {
66
+ if (!opts || typeof opts !== 'object') throw new TypeError('buildSkepticalPrompt: opts required');
67
+ if (typeof opts.specMarkdown !== 'string') throw new TypeError('buildSkepticalPrompt: specMarkdown required');
68
+
69
+ const dis = _isDisabled();
70
+ if (dis.disabled) {
71
+ return { systemPrompt: '', userPrompt: '', preChecks: {}, metadata: { skipped: true, reason: dis.reason, taskId: opts.taskId || null } };
72
+ }
73
+
74
+ const { specMarkdown, diffText = '', changedFiles = [], commitMessage = '', taskId = '', bundleText = '' } = opts;
75
+
76
+ // Run the mechanical pre-checks so the evaluator is grounded in data, not vibes.
77
+ const bel = verifyBELAgainstDelivery({ specMarkdown, diffText, changedFiles, commitMessage });
78
+ const bundle = verifySpecBundleCoverage({ specMarkdown, diffText, changedFiles, bundleText });
79
+ const belItems = parseBELItems(specMarkdown);
80
+ const specStrings = extractSpecStrings(specMarkdown);
81
+
82
+ const systemPrompt = [
83
+ '# Skeptical Evaluator',
84
+ '',
85
+ 'You are the Skeptical Evaluator for WogiFlow\'s validating phase. Your job is to force field-by-field enumeration before a task can be marked "done". Your baseline stance: the claim is unverified until you see every field enumerated and classified.',
86
+ '',
87
+ '## Inputs you receive',
88
+ '',
89
+ '- The task spec (markdown)',
90
+ '- The unified diff',
91
+ '- Changed file paths',
92
+ '- Commit message (if any)',
93
+ '- Mechanical pre-check results from `flow-completion-truth-gate` (BEL grep, spec-bundle grep)',
94
+ '',
95
+ '## Mandatory enumeration passes',
96
+ '',
97
+ '### Pass 1 — UI-field enumeration',
98
+ 'For every modified UI surface (form, filter, wizard, settings panel):',
99
+ '- List every `<input>`, `<select>`, `<textarea>`, or custom input component by its `name` / `data-testid`.',
100
+ '- For each: label, type, default, required, validation, visibility condition.',
101
+ '- Compare to the spec\'s AC. Flag vanished / modified / added fields.',
102
+ '',
103
+ 'If no UI surfaces touched, state explicitly: "UI-field pass: N/A — no UI files modified."',
104
+ 'Reference template: `' + path.relative(process.cwd(), TEMPLATE_PATH) + '`.',
105
+ '',
106
+ '### Pass 2 — API-parameter enumeration',
107
+ 'For every touched API endpoint (request handler, route, or client call):',
108
+ '- List every request parameter (query, path, body field).',
109
+ '- List every response field.',
110
+ '- Compare to the spec\'s AC. Flag additions / removals / type changes.',
111
+ '',
112
+ 'If no API work touched, state explicitly: "API-parameter pass: N/A."',
113
+ '',
114
+ '### Pass 3 — State-key enumeration',
115
+ 'For every touched state file (JSON, YAML, TOML, .env) or config key:',
116
+ '- List every top-level key and each nested key the change introduces / removes.',
117
+ '- Compare to the spec\'s AC.',
118
+ '',
119
+ 'If no state-file work touched, state explicitly: "State-key pass: N/A."',
120
+ '',
121
+ '## Evidence tier + confidence tier on every claim',
122
+ '',
123
+ 'For every enumerated item you classify as preserved/modified/added/vanished, attach:',
124
+ '- `evidenceTier`: 0–4 per `scripts/flow-runtime-verification.js` EVIDENCE_TIERS',
125
+ '- `confidencePct`: exactly 95, 85, or 75 per `.workflow/rubrics/confidence-tiers.md`',
126
+ '- `evidenceNote`: one-line citation (file:line, grep result, or observation)',
127
+ '',
128
+ 'Confidence 75 automatically flags the claim `UNVERIFIED`. Do not upgrade without evidence.',
129
+ '',
130
+ '## Output contract',
131
+ '',
132
+ 'Return ONE JSON object with shape:',
133
+ '```json',
134
+ '{',
135
+ ' "taskId": "<id>",',
136
+ ' "uiFieldPass": { "ran": true|false, "reason": "...", "findings": [...] },',
137
+ ' "apiParameterPass": { ... },',
138
+ ' "stateKeyPass": { ... },',
139
+ ' "overallVerdict": "PASS" | "CONCERN" | "FAIL",',
140
+ ' "blockers": ["one string per blocking issue"],',
141
+ ' "unverifiedClaims": ["one string per claim at confidence 75"]',
142
+ '}',
143
+ '```',
144
+ '',
145
+ 'No prose. No markdown fences around the JSON. Just the object.',
146
+ ].join('\n');
147
+
148
+ const userPrompt = [
149
+ '# Inputs',
150
+ '',
151
+ `- Task ID: ${taskId || '<unknown>'}`,
152
+ `- Changed files (${changedFiles.length}): ${changedFiles.slice(0, 20).join(', ')}${changedFiles.length > 20 ? ', ...' : ''}`,
153
+ '',
154
+ '## Spec',
155
+ '```markdown',
156
+ _truncate(specMarkdown, 12000),
157
+ '```',
158
+ '',
159
+ '## Unified diff',
160
+ '```',
161
+ _truncate(diffText, 12000),
162
+ '```',
163
+ '',
164
+ '## Commit message',
165
+ '```',
166
+ _truncate(commitMessage, 2000),
167
+ '```',
168
+ '',
169
+ '## Mechanical pre-checks (from flow-completion-truth-gate)',
170
+ '',
171
+ '### BEL grep',
172
+ `- items parsed: ${belItems.length}`,
173
+ `- ok: ${bel.ok}`,
174
+ `- uncovered: ${bel.uncoveredItems.length}`,
175
+ bel.uncoveredItems.length ? `- uncovered samples: ${bel.uncoveredItems.slice(0, 5).map((u) => u.text).join(' | ')}` : '',
176
+ '',
177
+ '### Spec-bundle coverage',
178
+ `- ok: ${bundle.ok}`,
179
+ ..._bundleSummaryLines(bundle),
180
+ '',
181
+ '## Extracted spec strings (reference)',
182
+ `- backtickIds (${specStrings.backtickIds.length}): ${specStrings.backtickIds.slice(0, 8).join(', ')}`,
183
+ `- filePaths (${specStrings.filePaths.length}): ${specStrings.filePaths.slice(0, 8).join(', ')}`,
184
+ `- constants (${specStrings.constants.length}): ${specStrings.constants.slice(0, 8).join(', ')}`,
185
+ `- routes (${specStrings.routes.length}): ${specStrings.routes.slice(0, 8).join(', ')}`,
186
+ '',
187
+ '## Your task',
188
+ '',
189
+ 'Run the three enumeration passes described in the system prompt. Return the JSON object. Be skeptical — force a verdict on every field.',
190
+ ].filter(Boolean).join('\n');
191
+
192
+ return {
193
+ systemPrompt,
194
+ userPrompt,
195
+ preChecks: { bel, bundle, belItems, specStrings },
196
+ metadata: { taskId: taskId || null, changedFileCount: changedFiles.length },
197
+ };
198
+ }
199
+
200
+ function _truncate(text, cap) {
201
+ const s = String(text || '');
202
+ return s.length > cap ? s.slice(0, cap) + `\n\n[... truncated at ${cap} chars]` : s;
203
+ }
204
+
205
+ function _bundleSummaryLines(bundle) {
206
+ const out = [];
207
+ for (const [cat, v] of Object.entries(bundle.coverage || {})) {
208
+ if (v.total === 0) continue;
209
+ out.push(` - ${cat}: ${v.hit}/${v.total} (need ${v.threshold.toFixed(2)})`);
210
+ if (v.missing && v.missing.length > 0) out.push(` missing: ${v.missing.slice(0, 4).join(', ')}`);
211
+ }
212
+ return out;
213
+ }
214
+
215
+ /**
216
+ * Parse the sub-agent's JSON response.
217
+ * @param {string} response
218
+ * @param {object} [ctx]
219
+ * @returns {object}
220
+ */
221
+ function parseSkepticalOutput(response, ctx = {}) {
222
+ if (typeof response !== 'string' || response.trim().length === 0) {
223
+ return { ok: false, reason: 'empty response', overallVerdict: 'FAIL' };
224
+ }
225
+ let parsed;
226
+ try {
227
+ // Try raw
228
+ parsed = JSON.parse(response);
229
+ } catch (_err) {
230
+ // Try extracting JSON object
231
+ const m = response.match(/\{[\s\S]*\}/);
232
+ if (!m) return { ok: false, reason: 'no JSON object found', overallVerdict: 'FAIL' };
233
+ try {
234
+ parsed = JSON.parse(m[0]);
235
+ } catch (err) {
236
+ return { ok: false, reason: `JSON parse failed: ${err.message}`, overallVerdict: 'FAIL' };
237
+ }
238
+ }
239
+ if (!parsed || typeof parsed !== 'object') {
240
+ return { ok: false, reason: 'response is not an object', overallVerdict: 'FAIL' };
241
+ }
242
+ const verdict = parsed.overallVerdict || 'FAIL';
243
+ return {
244
+ ok: verdict === 'PASS',
245
+ overallVerdict: verdict,
246
+ uiFieldPass: parsed.uiFieldPass || { ran: false },
247
+ apiParameterPass: parsed.apiParameterPass || { ran: false },
248
+ stateKeyPass: parsed.stateKeyPass || { ran: false },
249
+ blockers: Array.isArray(parsed.blockers) ? parsed.blockers : [],
250
+ unverifiedClaims: Array.isArray(parsed.unverifiedClaims) ? parsed.unverifiedClaims : [],
251
+ taskId: parsed.taskId || ctx.taskId || null,
252
+ };
253
+ }
254
+
255
+ module.exports = {
256
+ buildSkepticalPrompt,
257
+ parseSkepticalOutput,
258
+ TEMPLATE_PATH,
259
+ };
260
+
261
+ if (require.main === module) {
262
+ const cmd = process.argv[2];
263
+ if (cmd === 'prompt') {
264
+ const specFile = process.argv[3];
265
+ if (!specFile) { console.error('usage: flow-skeptical-evaluator prompt <spec.md>'); process.exit(2); }
266
+ const fs = require('node:fs');
267
+ const specMarkdown = fs.readFileSync(specFile, 'utf8');
268
+ const built = buildSkepticalPrompt({ specMarkdown, diffText: '', changedFiles: [], taskId: 'cli' });
269
+ console.log('--- SYSTEM ---\n' + built.systemPrompt + '\n--- USER ---\n' + built.userPrompt);
270
+ } else {
271
+ console.error('usage: flow-skeptical-evaluator prompt <spec.md>');
272
+ process.exit(2);
273
+ }
274
+ }
@@ -229,7 +229,7 @@ async function fetchDocsViaContext7(technology) {
229
229
  // SKILL FILE GENERATION
230
230
  // ============================================
231
231
 
232
- function generateSkillMd(tech, docs) {
232
+ function _generateSkillMd(tech, docs) {
233
233
  const date = getTodayDate();
234
234
 
235
235
  return `---
@@ -595,7 +595,7 @@ This skill is loaded on-demand when:
595
595
  * v2.0 format includes: type, ecosystem, loadWith, tokenCost
596
596
  */
597
597
  function generateSkillsIndex(technologies, selections) {
598
- const { getSkillType, getParentFramework, ECOSYSTEMS } = getTechOptions();
598
+ const { getSkillType, getParentFramework, _ECOSYSTEMS } = getTechOptions();
599
599
  const skills = {};
600
600
 
601
601
  // First pass: identify all technologies and their types
@@ -759,7 +759,7 @@ function migrateOldSkills(projectRoot) {
759
759
  * @param {Object} skillContext - Additional context { type, parentFramework, ecosystemSkills }
760
760
  */
761
761
  async function writeSkillFiles(tech, docs, projectRoot, skillContext = {}) {
762
- const { getSkillType, getParentFramework } = getTechOptions();
762
+ const { getSkillType, _getParentFramework } = getTechOptions();
763
763
 
764
764
  const skillId = tech.value.toLowerCase().replace(/[^a-z0-9]/g, '-');
765
765
  const skillDir = path.join(projectRoot, '.claude', 'skills', skillId);
@@ -29,9 +29,6 @@ function log(color, ...args) {
29
29
  console.log(colors[color] + args.join(' ') + colors.reset);
30
30
  }
31
31
 
32
- // Alias getConfig as loadConfig for minimal code changes
33
- const loadConfig = getConfig;
34
-
35
32
  function isLearningEnabled(config, trigger) {
36
33
  if (!config?.skillLearning?.enabled) return false;
37
34
  if (!config?.skillLearning?.autoExtract) return false;
@@ -81,7 +78,7 @@ function getChangedFiles(staged = false) {
81
78
  }
82
79
  }
83
80
 
84
- function getRecentCommitFiles(count = 1) {
81
+ function _getRecentCommitFiles(count = 1) {
85
82
  try {
86
83
  const cmd = `git diff HEAD~${count} --name-only`;
87
84
  const output = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
@@ -387,7 +384,7 @@ function formatSemanticChanges(semanticChanges) {
387
384
 
388
385
  const lines = [];
389
386
 
390
- for (const { file, changes } of semanticChanges) {
387
+ for (const { _file, changes } of semanticChanges) {
391
388
  if (changes.length === 0) continue;
392
389
 
393
390
  for (const change of changes) {
@@ -714,7 +711,7 @@ async function main() {
714
711
  process.exit(0);
715
712
  }
716
713
 
717
- const config = loadConfig();
714
+ const config = getConfig();
718
715
 
719
716
  if (!isLearningEnabled(config, options.trigger)) {
720
717
  if (options.verbose) {
@@ -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,