wogiflow 2.26.2 → 2.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +149 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-config-loader.js +3 -2
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-correction-detector.js +3 -2
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-prompt-composer.js +3 -2
- package/scripts/flow-prompt-template.js +3 -2
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-mcp-strip.js +122 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- package/.workflow/templates/partials/user-commands.hbs +0 -20
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Feature Dossier Gate (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* Auto-injects matching feature dossiers + cross-cutting logic rules into
|
|
7
|
+
* the phase context so Claude doesn't have to fetch them under token pressure.
|
|
8
|
+
*
|
|
9
|
+
* Problem this solves (2026-04-24 workspace failure catalog):
|
|
10
|
+
* - Claude doesn't proactively fetch context — grabs one thing, ignores rest
|
|
11
|
+
* - Feature/workflow knowledge is lost between sessions
|
|
12
|
+
* - Owner corrections stop being remembered
|
|
13
|
+
* - "I know the rule exists" ≠ "I consulted it before acting"
|
|
14
|
+
*
|
|
15
|
+
* Two enforcement surfaces:
|
|
16
|
+
* 1. buildPhaseInjection() — called by UserPromptSubmit. Returns a
|
|
17
|
+
* markdown block with the top matching dossiers' canonical content.
|
|
18
|
+
* Injected into the phase prompt alongside the phase-context injection.
|
|
19
|
+
*
|
|
20
|
+
* 2. validateSpecContradictions() — called from /wogi-story spec-review.
|
|
21
|
+
* Scans a spec file against every matching dossier and returns
|
|
22
|
+
* blocking issues (spec mentions rejected alternative, spec reintroduces
|
|
23
|
+
* removed element). Returns { blocked: boolean, issues: [...] }.
|
|
24
|
+
*
|
|
25
|
+
* Fail-open throughout. Missing dossiers, unreadable files, grep failures —
|
|
26
|
+
* all fail-open. The gate is an aid, not a hard stopgap; the core invariant
|
|
27
|
+
* is "don't break existing workflow if dossiers aren't set up yet."
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
const { PATHS, safeJsonParse, getConfig } = require('../../flow-utils');
|
|
33
|
+
|
|
34
|
+
function isEnabled() {
|
|
35
|
+
try {
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
return config.featureDossier?.enabled !== false;
|
|
38
|
+
} catch (_err) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getCurrentTaskInfo() {
|
|
44
|
+
try {
|
|
45
|
+
const ready = safeJsonParse(PATHS.ready, null);
|
|
46
|
+
if (!ready || !Array.isArray(ready.inProgress) || ready.inProgress.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const task = ready.inProgress[0];
|
|
50
|
+
const files = [];
|
|
51
|
+
if (task.specPath && fs.existsSync(path.join(PATHS.root, task.specPath))) {
|
|
52
|
+
files.push(task.specPath);
|
|
53
|
+
}
|
|
54
|
+
const changeFiles = listRecentlyChangedFiles();
|
|
55
|
+
return {
|
|
56
|
+
id: task.id,
|
|
57
|
+
title: task.title || '',
|
|
58
|
+
description: task.notes || task.description || '',
|
|
59
|
+
criteria: task.criteria || [],
|
|
60
|
+
files: [...new Set([...files, ...changeFiles])]
|
|
61
|
+
};
|
|
62
|
+
} catch (_err) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listRecentlyChangedFiles() {
|
|
68
|
+
try {
|
|
69
|
+
const { execSync } = require('node:child_process');
|
|
70
|
+
const out = execSync('git diff --name-only HEAD 2>/dev/null; git status --porcelain 2>/dev/null | awk \'{print $2}\' ', {
|
|
71
|
+
cwd: PATHS.root, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
|
|
72
|
+
});
|
|
73
|
+
return out.split('\n').map(s => s.trim()).filter(Boolean).slice(0, 100);
|
|
74
|
+
} catch (_err) { return []; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build the phase-injection block for the current task.
|
|
79
|
+
* Returns null if no matches, dossier system is disabled, or on any error.
|
|
80
|
+
*/
|
|
81
|
+
function getDossierInjection() {
|
|
82
|
+
if (!isEnabled()) return null;
|
|
83
|
+
let dossier, logicRules;
|
|
84
|
+
try {
|
|
85
|
+
dossier = require('../../flow-feature-dossier');
|
|
86
|
+
logicRules = require('../../flow-logic-rules');
|
|
87
|
+
} catch (_err) { return null; }
|
|
88
|
+
|
|
89
|
+
const taskInfo = getCurrentTaskInfo();
|
|
90
|
+
if (!taskInfo) return null;
|
|
91
|
+
|
|
92
|
+
const criteriaText = (taskInfo.criteria || []).map(c =>
|
|
93
|
+
typeof c === 'string' ? c : (c && c.text) || ''
|
|
94
|
+
).join('\n');
|
|
95
|
+
const matchInput = {
|
|
96
|
+
title: taskInfo.title,
|
|
97
|
+
description: `${taskInfo.description}\n${criteriaText}`,
|
|
98
|
+
files: taskInfo.files
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let featureBlock = null, rulesBlock = null;
|
|
102
|
+
try {
|
|
103
|
+
const featureMatches = dossier.matchFeatures(matchInput);
|
|
104
|
+
const config = getConfig();
|
|
105
|
+
const minScore = config.featureDossier?.autoMatchConfidence ?? 1;
|
|
106
|
+
featureBlock = dossier.buildPhaseInjection(featureMatches, { minScore, maxDossiers: 3 });
|
|
107
|
+
} catch (_err) { /* non-blocking */ }
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const ruleMatches = logicRules.matchRulesForFiles(
|
|
111
|
+
taskInfo.files,
|
|
112
|
+
[taskInfo.title, taskInfo.description].filter(Boolean)
|
|
113
|
+
);
|
|
114
|
+
rulesBlock = logicRules.buildRulesInjection(ruleMatches);
|
|
115
|
+
} catch (_err) { /* non-blocking */ }
|
|
116
|
+
|
|
117
|
+
const parts = [featureBlock, rulesBlock].filter(Boolean);
|
|
118
|
+
if (parts.length === 0) return null;
|
|
119
|
+
return parts.join('\n\n---\n\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate a spec file against all matching dossiers.
|
|
124
|
+
* Returns { blocked, issues } — blocked=true if any blocker-severity issue.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} specContent
|
|
127
|
+
* @param {Object} [taskInfo] — optional pre-computed task info; otherwise detected
|
|
128
|
+
*/
|
|
129
|
+
function validateSpecContradictions(specContent, taskInfo) {
|
|
130
|
+
if (!isEnabled()) return { blocked: false, issues: [] };
|
|
131
|
+
let dossier;
|
|
132
|
+
try { dossier = require('../../flow-feature-dossier'); }
|
|
133
|
+
catch (_err) { return { blocked: false, issues: [] }; }
|
|
134
|
+
|
|
135
|
+
const info = taskInfo || getCurrentTaskInfo();
|
|
136
|
+
if (!info) return { blocked: false, issues: [] };
|
|
137
|
+
|
|
138
|
+
const matches = dossier.matchFeatures({
|
|
139
|
+
title: info.title,
|
|
140
|
+
description: info.description,
|
|
141
|
+
files: info.files
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const allIssues = [];
|
|
145
|
+
for (const m of matches) {
|
|
146
|
+
const d = dossier.loadDossier(m.slug);
|
|
147
|
+
if (!d) continue;
|
|
148
|
+
const issues = dossier.validateSpecAgainstDossier(specContent, d);
|
|
149
|
+
for (const issue of issues) {
|
|
150
|
+
allIssues.push({ ...issue, dossier: m.slug });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let blockOnContradiction = true;
|
|
155
|
+
try {
|
|
156
|
+
const config = getConfig();
|
|
157
|
+
blockOnContradiction = config.featureDossier?.blockOnContradiction !== false;
|
|
158
|
+
} catch (_err) { /* default true */ }
|
|
159
|
+
|
|
160
|
+
const blockers = allIssues.filter(i => i.severity === 'blocker');
|
|
161
|
+
return {
|
|
162
|
+
blocked: blockOnContradiction && blockers.length > 0,
|
|
163
|
+
issues: allIssues
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Format contradictions as a block message the caller can surface to the user.
|
|
169
|
+
*/
|
|
170
|
+
function formatContradictionMessage(issues) {
|
|
171
|
+
if (!issues || issues.length === 0) return '';
|
|
172
|
+
const lines = ['## Feature Dossier Contradiction Gate', ''];
|
|
173
|
+
lines.push(`${issues.length} contradiction(s) found between the proposed spec and one or more active feature dossiers.`);
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push('Dossiers capture owner-rejected alternatives and removed elements. A spec that reintroduces any of these would re-introduce a bug the owner already corrected.');
|
|
176
|
+
lines.push('');
|
|
177
|
+
for (const issue of issues) {
|
|
178
|
+
lines.push(`- **[${issue.severity}]** (${issue.dossier} / ${issue.kind}) ${issue.detail}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push('**Resolution**:');
|
|
182
|
+
lines.push('1. Read the referenced dossier section in full.');
|
|
183
|
+
lines.push('2. If the owner has actually changed their mind, update the dossier (move the item out of Rejected / Removed) before proceeding.');
|
|
184
|
+
lines.push('3. Otherwise, revise the spec to not reintroduce the rejected/removed item.');
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
isEnabled,
|
|
190
|
+
getCurrentTaskInfo,
|
|
191
|
+
getDossierInjection,
|
|
192
|
+
validateSpecContradictions,
|
|
193
|
+
formatContradictionMessage
|
|
194
|
+
};
|
|
@@ -376,6 +376,27 @@ async function captureObservation(options) {
|
|
|
376
376
|
// Exports
|
|
377
377
|
// ============================================================
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Pick the authoritative tool-execution duration.
|
|
381
|
+
*
|
|
382
|
+
* Claude Code 2.1.119+ attaches a numeric `duration_ms` to PostToolUse /
|
|
383
|
+
* PostToolUseFailure payloads — real tool execution time, excluding
|
|
384
|
+
* permission prompts and PreToolUse hooks. Older CC versions omit it, so
|
|
385
|
+
* callers pass a locally-computed fallback (typically near-zero because
|
|
386
|
+
* the hook only measures the gap between adjacent statements, not the
|
|
387
|
+
* tool itself). Prefer native when numeric; fall back otherwise.
|
|
388
|
+
*
|
|
389
|
+
* @param {object} parsedInput - hook payload
|
|
390
|
+
* @param {number} fallbackMs - local duration to use when native is absent
|
|
391
|
+
* @returns {number}
|
|
392
|
+
*/
|
|
393
|
+
function selectDuration(parsedInput, fallbackMs) {
|
|
394
|
+
if (parsedInput && typeof parsedInput.duration_ms === 'number') {
|
|
395
|
+
return parsedInput.duration_ms;
|
|
396
|
+
}
|
|
397
|
+
return fallbackMs;
|
|
398
|
+
}
|
|
399
|
+
|
|
379
400
|
module.exports = {
|
|
380
401
|
// Configuration
|
|
381
402
|
isObservationCaptureEnabled,
|
|
@@ -387,6 +408,9 @@ module.exports = {
|
|
|
387
408
|
summarizeInput,
|
|
388
409
|
summarizeOutput,
|
|
389
410
|
|
|
411
|
+
// Duration source selection (CC 2.1.119+ native, fallback otherwise)
|
|
412
|
+
selectDuration,
|
|
413
|
+
|
|
390
414
|
// Main capture function
|
|
391
415
|
captureObservation
|
|
392
416
|
};
|
|
@@ -259,10 +259,29 @@ function buildOverdueContext(opts = {}) {
|
|
|
259
259
|
return lostBlock;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
|
|
262
|
+
// Worker completion summaries (Story B / wf-ab59f0e4): surface unseen
|
|
263
|
+
// summaries from any worker that finished an autonomous epic. Render via
|
|
264
|
+
// flow-workspace-summary.renderMultiWorker for the multi-worker block.
|
|
265
|
+
let summariesBlock = null;
|
|
266
|
+
let seenTaskIds = [];
|
|
267
|
+
try {
|
|
268
|
+
const libPath = path.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-dispatch-tracking.js');
|
|
269
|
+
const { readPendingCompletionSummaries, markCompletionSummariesSeen } = require(libPath);
|
|
270
|
+
const pending = readPendingCompletionSummaries(workspaceRoot);
|
|
271
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
272
|
+
const wsSummary = require(path.resolve(__dirname, '..', '..', 'flow-workspace-summary.js'));
|
|
273
|
+
const payloads = pending.map(p => p.summary);
|
|
274
|
+
summariesBlock = wsSummary.renderMultiWorker(payloads);
|
|
275
|
+
seenTaskIds = pending.map(p => p.taskId);
|
|
276
|
+
markCompletionSummariesSeen(workspaceRoot, seenTaskIds);
|
|
277
|
+
}
|
|
278
|
+
} catch (_err) { /* fail-open — surface what we can */ }
|
|
279
|
+
|
|
280
|
+
if ((!Array.isArray(overdue) || overdue.length === 0) && !lostBlock && !summariesBlock) return null;
|
|
263
281
|
|
|
264
282
|
const sections = [];
|
|
265
283
|
|
|
284
|
+
if (summariesBlock) sections.push(summariesBlock);
|
|
266
285
|
if (lostBlock) sections.push(lostBlock);
|
|
267
286
|
|
|
268
287
|
if (Array.isArray(overdue) && overdue.length > 0) {
|
|
@@ -186,12 +186,26 @@ function transitionPhase(from, to, taskId) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
const wrote = writePhaseState({
|
|
190
190
|
phase: to,
|
|
191
191
|
taskId: taskId || current.taskId,
|
|
192
192
|
updatedAt: new Date().toISOString(),
|
|
193
193
|
previousPhase: from
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
// Clear any stale routing-pending flag left over from a prior turn when
|
|
197
|
+
// crossing a phase boundary. Uses removeRoutingFlag() (not clearRoutingPending)
|
|
198
|
+
// so the cleared-marker is preserved — we don't want to open a 15s bypass
|
|
199
|
+
// window just because a phase transitioned. Fail-open if the module is
|
|
200
|
+
// unavailable (e.g. future CLI adapter without routing-gate wired).
|
|
201
|
+
if (wrote) {
|
|
202
|
+
try {
|
|
203
|
+
const { removeRoutingFlag } = require('./routing-gate');
|
|
204
|
+
removeRoutingFlag();
|
|
205
|
+
} catch (_err) { /* fail-open */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return wrote;
|
|
195
209
|
}
|
|
196
210
|
|
|
197
211
|
/**
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase-transition Auto-Review trigger (wf-8d635d0e / E1).
|
|
5
|
+
*
|
|
6
|
+
* Called after a successful phase transition. When the transition is
|
|
7
|
+
* `coding → validating` and `config.autoReview.enabled`, fire a detached
|
|
8
|
+
* background review worker. Non-blocking by design (AC5) — parent returns
|
|
9
|
+
* immediately; the worker writes findings asynchronously.
|
|
10
|
+
*
|
|
11
|
+
* Failure-mode: any error from the spawn path is swallowed and logged via
|
|
12
|
+
* DEBUG only. Auto-review is an advisory signal layered on top of the
|
|
13
|
+
* existing Completion Truth Gate — a crash here must never fail the
|
|
14
|
+
* primary phase transition.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { getConfig } = require('../../flow-utils');
|
|
18
|
+
|
|
19
|
+
function isAutoReviewEnabled(cfg) {
|
|
20
|
+
try {
|
|
21
|
+
const c = cfg || getConfig();
|
|
22
|
+
return c?.autoReview?.enabled === true;
|
|
23
|
+
} catch (_err) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Trigger a background auto-review when appropriate.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} from - prior phase
|
|
32
|
+
* @param {string} to - new phase
|
|
33
|
+
* @param {string} taskId
|
|
34
|
+
* @param {Object} [opts]
|
|
35
|
+
* @param {Function} [opts.starter] — injectable startReview (tests)
|
|
36
|
+
* @returns {{ started:boolean, reason?:string, handle?:Object }}
|
|
37
|
+
*/
|
|
38
|
+
function maybeStartAutoReview(from, to, taskId, opts = {}) {
|
|
39
|
+
if (from !== 'coding' || to !== 'validating') {
|
|
40
|
+
return { started: false, reason: 'not-validating-transition' };
|
|
41
|
+
}
|
|
42
|
+
if (!taskId) {
|
|
43
|
+
return { started: false, reason: 'no-task-id' };
|
|
44
|
+
}
|
|
45
|
+
if (!isAutoReviewEnabled(opts.config)) {
|
|
46
|
+
return { started: false, reason: 'disabled' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const starter = opts.starter || require('../../../lib/worktree-review').startReview;
|
|
50
|
+
try {
|
|
51
|
+
const handle = starter({ taskId, repoRoot: opts.repoRoot });
|
|
52
|
+
return { started: true, handle };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (process.env.DEBUG) {
|
|
55
|
+
console.error(`[auto-review] startReview failed: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
return { started: false, reason: 'spawn-error', error: String(err.message || err) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { maybeStartAutoReview, isAutoReviewEnabled };
|
|
@@ -184,10 +184,13 @@ function handlePostCompact() {
|
|
|
184
184
|
|
|
185
185
|
// 3. Re-set routing-pending flag
|
|
186
186
|
// After compaction, the AI has fresh context and may try to act without routing.
|
|
187
|
-
//
|
|
187
|
+
// resetRoutingState() first clears any stale cleared-marker from the pre-compact
|
|
188
|
+
// turn — otherwise setRoutingPending() would short-circuit on the 15s skill-chain
|
|
189
|
+
// suppression window and leave the flag unset, creating a bypass.
|
|
188
190
|
let routingReArmed = false;
|
|
189
191
|
try {
|
|
190
|
-
const { setRoutingPending } = require('./routing-gate');
|
|
192
|
+
const { setRoutingPending, resetRoutingState } = require('./routing-gate');
|
|
193
|
+
resetRoutingState();
|
|
191
194
|
setRoutingPending();
|
|
192
195
|
routingReArmed = true;
|
|
193
196
|
} catch (err) {
|
|
@@ -261,6 +261,27 @@ function runPreToolGates(ctx, deps) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// Path-discipline gate (Story B / wf-ab59f0e4): runs in BOTH manager and
|
|
265
|
+
// worker mode (different rules each side). Cross-process state writes are
|
|
266
|
+
// blocked fail-loud so file corruption is impossible to ignore.
|
|
267
|
+
if (process.env.WOGI_WORKSPACE_ROOT) {
|
|
268
|
+
try {
|
|
269
|
+
if (typeof deps.checkPathDiscipline === 'function') {
|
|
270
|
+
const pathResult = deps.checkPathDiscipline(toolName, toolInput);
|
|
271
|
+
if (pathResult.blocked) {
|
|
272
|
+
return {
|
|
273
|
+
allowed: false,
|
|
274
|
+
blocked: true,
|
|
275
|
+
reason: pathResult.reason,
|
|
276
|
+
message: pathResult.message,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (process.env.DEBUG) console.error(`[Hook] Path discipline gate error (fail-open): ${err.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
264
285
|
// Commit log gate
|
|
265
286
|
if (toolName === 'Bash' && toolInput.command) {
|
|
266
287
|
try {
|
|
@@ -385,6 +385,62 @@ function incrementStopAttempts(maxAttempts = 10) {
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Remove the routing-pending flag file WITHOUT writing a cleared-marker.
|
|
390
|
+
*
|
|
391
|
+
* Distinct from clearRoutingPending(): that writes a 15s cleared-marker to
|
|
392
|
+
* suppress re-setting during skill chains. This helper just deletes the flag
|
|
393
|
+
* so stale pending state (e.g. from a crashed prior turn) can't block tools
|
|
394
|
+
* on the next check. The cleared-marker path is left untouched — so if a
|
|
395
|
+
* /wogi-* skill is actively executing, its suppression window is preserved.
|
|
396
|
+
*
|
|
397
|
+
* Used by phase transitions: moving between phases should not leave stale
|
|
398
|
+
* pending flags blocking tools, but must not create a bypass window either.
|
|
399
|
+
*
|
|
400
|
+
* @returns {{ removed: boolean }}
|
|
401
|
+
*/
|
|
402
|
+
function removeRoutingFlag() {
|
|
403
|
+
try {
|
|
404
|
+
fs.unlinkSync(ROUTING_FLAG_PATH);
|
|
405
|
+
return { removed: true };
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err.code === 'ENOENT') return { removed: true };
|
|
408
|
+
if (process.env.DEBUG) {
|
|
409
|
+
console.error(`[routing-gate] removeRoutingFlag failed: ${err.message}`);
|
|
410
|
+
}
|
|
411
|
+
return { removed: false };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Reset routing state entirely — delete BOTH the pending flag and the
|
|
417
|
+
* cleared-marker. Leaves a clean slate so the next UserPromptSubmit re-arms
|
|
418
|
+
* the gate normally without any inherited bypass window.
|
|
419
|
+
*
|
|
420
|
+
* Used by PostCompact: compaction produces fresh context, so any in-flight
|
|
421
|
+
* skill-chain suppression or stale pending flag from the pre-compact turn
|
|
422
|
+
* is no longer relevant. Caller is responsible for re-arming via
|
|
423
|
+
* setRoutingPending() if the post-reset state needs a pending flag.
|
|
424
|
+
*
|
|
425
|
+
* @returns {{ reset: boolean }}
|
|
426
|
+
*/
|
|
427
|
+
function resetRoutingState() {
|
|
428
|
+
let ok = true;
|
|
429
|
+
for (const p of [ROUTING_FLAG_PATH, ROUTING_CLEARED_PATH]) {
|
|
430
|
+
try {
|
|
431
|
+
fs.unlinkSync(p);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (err.code !== 'ENOENT') {
|
|
434
|
+
ok = false;
|
|
435
|
+
if (process.env.DEBUG) {
|
|
436
|
+
console.error(`[routing-gate] resetRoutingState unlink ${p} failed: ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { reset: ok };
|
|
442
|
+
}
|
|
443
|
+
|
|
388
444
|
module.exports = {
|
|
389
445
|
isRoutingGateEnabled,
|
|
390
446
|
hasActiveTask,
|
|
@@ -394,6 +450,8 @@ module.exports = {
|
|
|
394
450
|
isRoutingRecentlyCleared,
|
|
395
451
|
checkRoutingGate,
|
|
396
452
|
incrementStopAttempts,
|
|
453
|
+
removeRoutingFlag,
|
|
454
|
+
resetRoutingState,
|
|
397
455
|
ROUTING_FLAG_PATH,
|
|
398
456
|
ROUTING_CLEARED_PATH
|
|
399
457
|
};
|
|
@@ -138,6 +138,12 @@ const KNOWN_STATE_FILES = new Set([
|
|
|
138
138
|
'.routing-pending',
|
|
139
139
|
'.routing-cleared',
|
|
140
140
|
'.gates-passed.json',
|
|
141
|
+
|
|
142
|
+
// Task-boundary restart machinery (wf-39e9dc09, R-336, wf-f267ea2a)
|
|
143
|
+
'task-just-completed',
|
|
144
|
+
'task-boundary-last-triggered',
|
|
145
|
+
'task-boundary-clean-completion.json',
|
|
146
|
+
'pending-question.json',
|
|
141
147
|
]);
|
|
142
148
|
|
|
143
149
|
/** Known directory names within state/ (not files) */
|
|
@@ -871,6 +877,108 @@ function formatContextForInjection(context) {
|
|
|
871
877
|
// Non-critical — history file may not exist; continue with normal context
|
|
872
878
|
}
|
|
873
879
|
|
|
880
|
+
// AUTO-PICKUP after clean completion (wf-f267ea2a).
|
|
881
|
+
// When the prior task completed cleanly AND the ready queue is non-empty AND
|
|
882
|
+
// no pending-question marker exists, instruct the AI to immediately invoke
|
|
883
|
+
// /wogi-start <nextReadyId> on the first user message rather than asking
|
|
884
|
+
// "what's next?". This is the main-mode mirror of workspace.autoPickupChannelDispatches.
|
|
885
|
+
//
|
|
886
|
+
// Marker is consumed (deleted) on every SessionStart that observes it,
|
|
887
|
+
// regardless of whether AUTO-PICKUP fires — so a stale marker can't loop
|
|
888
|
+
// across unrelated future restarts. Fail-open throughout: any error or
|
|
889
|
+
// missing config falls back to the default "proceed with next instruction".
|
|
890
|
+
try {
|
|
891
|
+
const cleanMarkerPath = path.join(PATHS.state, 'task-boundary-clean-completion.json');
|
|
892
|
+
if (fs.existsSync(cleanMarkerPath)) {
|
|
893
|
+
const config = getConfig();
|
|
894
|
+
const tbr = config.taskBoundaryReset || {};
|
|
895
|
+
const flagEnabled = tbr.autoPickupNextTask !== false; // default true
|
|
896
|
+
const pendingQuestionPath = path.join(PATHS.state, 'pending-question.json');
|
|
897
|
+
const hasPendingQuestion = fs.existsSync(pendingQuestionPath);
|
|
898
|
+
|
|
899
|
+
// Read the marker for diagnostic context (which task completed)
|
|
900
|
+
const markerPayload = safeJsonParse(cleanMarkerPath, null);
|
|
901
|
+
|
|
902
|
+
// Find next ready task. Prefer the first non-epic task — epics are
|
|
903
|
+
// containers whose work lives in their child stories, and auto-picking an
|
|
904
|
+
// epic produces a restart loop (wogi-start on an epic has no actionable
|
|
905
|
+
// next step when its children are not yet ready-queue tasks). If the
|
|
906
|
+
// queue contains only epics, emit no auto-pickup (safer to stop the loop
|
|
907
|
+
// than to re-enter it).
|
|
908
|
+
//
|
|
909
|
+
// Cascade-after-decomposition (Story E / wf-e28b6cd8): when the marker
|
|
910
|
+
// carries a `nextTaskId` (set by the cascade helper after epic
|
|
911
|
+
// decomposition), use that explicitly. Verify the ID still exists in
|
|
912
|
+
// ready.json before honoring it; if missing/stale, fall back to default
|
|
913
|
+
// first-actionable resolution.
|
|
914
|
+
let nextTaskId = null;
|
|
915
|
+
let nextTaskTitle = null;
|
|
916
|
+
try {
|
|
917
|
+
const ready = getReadyData();
|
|
918
|
+
const queue = Array.isArray(ready?.ready) ? ready.ready : [];
|
|
919
|
+
if (markerPayload && markerPayload.nextTaskId) {
|
|
920
|
+
const explicit = queue.find(t => t && t.id === markerPayload.nextTaskId);
|
|
921
|
+
if (explicit) {
|
|
922
|
+
nextTaskId = explicit.id;
|
|
923
|
+
nextTaskTitle = explicit.title || null;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (!nextTaskId) {
|
|
927
|
+
const firstActionable = queue.find(t => t && t.type !== 'epic');
|
|
928
|
+
if (firstActionable) {
|
|
929
|
+
nextTaskId = firstActionable.id || null;
|
|
930
|
+
nextTaskTitle = firstActionable.title || null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} catch (_err) { /* fall through — no auto-pickup if ready.json unreadable */ }
|
|
934
|
+
|
|
935
|
+
const shouldInject = flagEnabled && !hasPendingQuestion && nextTaskId;
|
|
936
|
+
|
|
937
|
+
if (shouldInject) {
|
|
938
|
+
output += `### ⚡ AUTO-PICKUP MODE ACTIVE\n`;
|
|
939
|
+
output += `Prior task **${markerPayload?.completedTaskId || 'unknown'}** (${markerPayload?.completedTaskTitle || 'no title'}) completed cleanly. The next ready task is **${nextTaskId}** — ${nextTaskTitle || '(no title)'}.\n\n`;
|
|
940
|
+
output += `**ON THE FIRST USER MESSAGE IN THIS SESSION (regardless of content): immediately invoke \`Skill(skill="wogi-start", args="${nextTaskId}")\`.**\n\n`;
|
|
941
|
+
output += `Do NOT ask "what's next?". Do NOT summarize prior work. Do NOT propose alternatives. The user has authorized autonomous continuation across this epic. If you have a question that genuinely cannot be resolved by self-challenge, use \`flow ask "<question>"\` so the next restart defers correctly (R-336).\n\n`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Always consume the marker (single-use), regardless of whether we injected
|
|
945
|
+
try { fs.unlinkSync(cleanMarkerPath); } catch (_err) { /* best effort */ }
|
|
946
|
+
}
|
|
947
|
+
} catch (_err) {
|
|
948
|
+
// Non-critical — fall through to default context
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Autonomous walk-away mode rehydration (wf-d712002e / Story C).
|
|
952
|
+
// Disk is canonical; cache evaporates at SIGTERM. SessionStart re-hydrates
|
|
953
|
+
// the cache from disk and surfaces the active-mode reminder so the AI
|
|
954
|
+
// continues to honor autonomous routing across task-boundary restarts.
|
|
955
|
+
// Stale-flag detection (older than autonomousMode.stalenessThresholdMs,
|
|
956
|
+
// default 1h) emits an interruption notice and clears the flag — no
|
|
957
|
+
// auto-resume; the user must explicitly say "continue" / "resume".
|
|
958
|
+
try {
|
|
959
|
+
const sessionState = require('../../flow-session-state');
|
|
960
|
+
const result = sessionState.rehydrateAutonomousFromDisk();
|
|
961
|
+
if (result.hydrated && result.mode) {
|
|
962
|
+
const mode = result.mode;
|
|
963
|
+
output += `### ⚡ AUTONOMOUS MODE ACTIVE\n`;
|
|
964
|
+
output += `Walk-away run \`${mode.runId}\` is in progress (trigger: "${mode.trigger}", started ${mode.activatedAt}).\n\n`;
|
|
965
|
+
output += `**Routing rules** for this run:\n`;
|
|
966
|
+
output += `- productBehavior / ux questions → \`queue-for-review\` (do NOT ask the user; \`flow-question-queue\` collects them).\n`;
|
|
967
|
+
output += `- engineering / naming / implementation → decide autonomously.\n`;
|
|
968
|
+
output += `- low-confidence technical → self-adversarial challenge to ≥90% confidence; if still uncertain after the cap, queue.\n`;
|
|
969
|
+
output += `- Blocking errors (typecheck/test/conflict) → fix autonomously; only surface if fundamentally un-fixable.\n\n`;
|
|
970
|
+
output += `**No hedging**. Forbidden phrases (per feedback-patterns.md 2026-04-16): "let me know if", "should I continue", "awaiting your signal", "standing by", "would you like me to". The user is walked away; there is no one to answer until the run ends.\n\n`;
|
|
971
|
+
output += `**Exit conditions**: ready queue drains, user types "stop"/"pause", or fatal error. On exit: render completion summary (terminal block + JSON payload at \`autonomous-run-summary-${mode.runId}.json\`).\n\n`;
|
|
972
|
+
} else if (result.reason === 'stale' && result.staleMode) {
|
|
973
|
+
const stale = result.staleMode;
|
|
974
|
+
output += `### ⚠️ Autonomous Run Interrupted\n`;
|
|
975
|
+
output += `A previous autonomous run (\`${stale.runId}\`, started ${stale.activatedAt}) exceeded the staleness threshold and has been cleared.\n\n`;
|
|
976
|
+
output += `Review \`.workflow/state/autonomous-run-summary-${stale.runId}.json\` (if present) to see what completed before the interruption. The session is now in standard interactive mode. To resume, the user must explicitly say "continue" / "resume autonomous run" — do not auto-restart.\n\n`;
|
|
977
|
+
}
|
|
978
|
+
} catch (_err) {
|
|
979
|
+
// Non-critical — autonomous-mode injection failure must not block session start
|
|
980
|
+
}
|
|
981
|
+
|
|
874
982
|
// Workspace worker auto-resume (wf-restart-handoff / 2.22.2).
|
|
875
983
|
// CRITICAL priority — shown at the top so the model acts on it before
|
|
876
984
|
// anything else. Fires when a worker session starts with queued channel
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Session End: Memory Proposal Surfacing
|
|
5
|
+
*
|
|
6
|
+
* Reads pending IGR-artifact edit proposals staged by `flow memory propose`
|
|
7
|
+
* and returns a structured summary for the session-end adapter to display.
|
|
8
|
+
*
|
|
9
|
+
* Agent-proposed edits do NOT auto-apply. The user reviews at session-end
|
|
10
|
+
* and runs `flow memory approve <id>` / `flow memory reject <id>`.
|
|
11
|
+
*
|
|
12
|
+
* Story: wf-4434851f (IGR artifact edit proposals).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const store = require('../../../lib/memory-proposal-store');
|
|
16
|
+
|
|
17
|
+
function summarizePendingMemoryProposals() {
|
|
18
|
+
let pending;
|
|
19
|
+
try {
|
|
20
|
+
pending = store.listProposals({ status: 'pending' });
|
|
21
|
+
} catch (_err) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (!pending || pending.length === 0) return null;
|
|
25
|
+
|
|
26
|
+
const byOp = { append: 0, 'replace-section': 0, 'replace-all': 0 };
|
|
27
|
+
const byBlock = {};
|
|
28
|
+
const previews = [];
|
|
29
|
+
for (const p of pending) {
|
|
30
|
+
byOp[p.op] = (byOp[p.op] || 0) + 1;
|
|
31
|
+
byBlock[p.block] = (byBlock[p.block] || 0) + 1;
|
|
32
|
+
try {
|
|
33
|
+
previews.push(store.previewProposal(p));
|
|
34
|
+
} catch (_err) {
|
|
35
|
+
// Non-fatal — skip a broken preview but keep the rest.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
count: pending.length,
|
|
41
|
+
byOp,
|
|
42
|
+
byBlock,
|
|
43
|
+
proposals: pending,
|
|
44
|
+
previews,
|
|
45
|
+
message: formatMessage(pending.length, byOp, previews),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatMessage(count, byOp, previews) {
|
|
50
|
+
const plural = count !== 1 ? 's' : '';
|
|
51
|
+
const breakdown = Object.entries(byOp)
|
|
52
|
+
.filter(([, n]) => n > 0)
|
|
53
|
+
.map(([op, n]) => `${n} ${op}`)
|
|
54
|
+
.join(', ');
|
|
55
|
+
return [
|
|
56
|
+
`${count} pending memory proposal${plural} (${breakdown}):`,
|
|
57
|
+
...previews,
|
|
58
|
+
'',
|
|
59
|
+
'Review: flow memory list',
|
|
60
|
+
'Approve: flow memory approve <id>',
|
|
61
|
+
'Reject: flow memory reject <id> [--reason <text>]',
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { summarizePendingMemoryProposals };
|