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.
- 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 +84 -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-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-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-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-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,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
|
|
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,
|
|
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,
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
|
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,
|