wogiflow 2.16.0 → 2.17.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/.workflow/templates/claude-md.hbs +1 -0
- package/lib/workspace.js +45 -2
- package/package.json +1 -1
- package/scripts/flow +3 -0
- package/scripts/flow-ask.js +121 -0
- package/scripts/flow-config-defaults.js +10 -0
- package/scripts/flow-constants.js +3 -1
- package/scripts/hooks/core/session-context.js +35 -0
- package/scripts/hooks/core/task-boundary-reset.js +10 -0
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +12 -0
|
@@ -274,6 +274,7 @@ When creating tasks programmatically, always call `generateTaskId(title)` — ne
|
|
|
274
274
|
2. Registry maps (app-map, function-map, api-map, schema-map, service-map) are **auto-updated** by the `registryUpdate` quality gate — it runs `flow registry-manager scan` on all active registries
|
|
275
275
|
3. Run quality gates (lint, typecheck, test)
|
|
276
276
|
4. Provide completion report
|
|
277
|
+
5. **If you have a follow-up question for the user** (e.g., "task done — should I also update X?"), run `flow ask "<your question>"` BEFORE the turn ends. This defers the task-boundary session restart (if enabled via `taskBoundaryReset.enabled`) so your question doesn't get orphaned when claude restarts. The user's response automatically clears the deferral.
|
|
277
278
|
|
|
278
279
|
## Auto-Validation (CRITICAL)
|
|
279
280
|
|
package/lib/workspace.js
CHANGED
|
@@ -18,6 +18,44 @@
|
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const path = require('node:path');
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* wf-f747f993 — resolve the claude-spawning command for workspace sessions.
|
|
23
|
+
*
|
|
24
|
+
* Policy (from spec wf-f747f993 S2):
|
|
25
|
+
* - Worker sessions: prefer `wogi-claude` (the wogiflow restart wrapper)
|
|
26
|
+
* when the wrapper file exists at its known package location. If
|
|
27
|
+
* taskBoundaryReset.enabled is false (the default), the wrapper still
|
|
28
|
+
* loops cleanly with no behavior change. If true, workers benefit from
|
|
29
|
+
* per-task context resets.
|
|
30
|
+
* - Manager sessions: always use `claude` directly. The manager coordinates
|
|
31
|
+
* workers; restart-looping the manager would orchestrate-storm the whole
|
|
32
|
+
* workspace.
|
|
33
|
+
* - Fallback: if the wrapper file isn't found (unusual install), fall back
|
|
34
|
+
* to `claude` silently.
|
|
35
|
+
*
|
|
36
|
+
* Detection: we resolve the wrapper by its location relative to this file
|
|
37
|
+
* (`__dirname/wogi-claude`) rather than `command -v` / PATH lookup, because
|
|
38
|
+
* workspace commands may execute without `node_modules/.bin` on PATH.
|
|
39
|
+
*
|
|
40
|
+
* @param {'manager'|'worker'} role
|
|
41
|
+
* @param {string} flags - the CLI flags to append
|
|
42
|
+
* @returns {string} the full shell command to exec
|
|
43
|
+
*/
|
|
44
|
+
function resolveClaudeSpawnCommand(role, flags) {
|
|
45
|
+
if (role === 'manager') {
|
|
46
|
+
return `claude ${flags}`;
|
|
47
|
+
}
|
|
48
|
+
// Worker — wrapper is shipped at lib/wogi-claude alongside this file
|
|
49
|
+
const wrapperPath = path.join(__dirname, 'wogi-claude');
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(wrapperPath)) {
|
|
52
|
+
// Quote the path in case of spaces, use absolute path so it works regardless of PATH
|
|
53
|
+
return `"${wrapperPath}" ${flags}`;
|
|
54
|
+
}
|
|
55
|
+
} catch (_err) { /* fall through */ }
|
|
56
|
+
return `claude ${flags}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
21
59
|
// ============================================================
|
|
22
60
|
// Constants
|
|
23
61
|
// ============================================================
|
|
@@ -1609,7 +1647,9 @@ function startWorkerSession(cwd) {
|
|
|
1609
1647
|
};
|
|
1610
1648
|
|
|
1611
1649
|
try {
|
|
1612
|
-
|
|
1650
|
+
// Manager uses claude directly — see resolveClaudeSpawnCommand + wf-f747f993 S2.
|
|
1651
|
+
const managerCmd = resolveClaudeSpawnCommand('manager', '--dangerously-skip-permissions --dangerously-load-development-channels server:wogi-workspace-channel');
|
|
1652
|
+
execSync(managerCmd, {
|
|
1613
1653
|
cwd,
|
|
1614
1654
|
env,
|
|
1615
1655
|
stdio: 'inherit'
|
|
@@ -1671,8 +1711,11 @@ function startWorkerSession(cwd) {
|
|
|
1671
1711
|
// Launch Claude Code with experimental channel support enabled.
|
|
1672
1712
|
// The --dangerously-load-development-channels flag makes Claude Code
|
|
1673
1713
|
// surface notifications/claude/channel from the MCP server as prompts.
|
|
1714
|
+
// Worker uses the wogi-claude wrapper when available (wf-f747f993) so
|
|
1715
|
+
// task-boundary session restart works for workers if opted-in via config.
|
|
1674
1716
|
try {
|
|
1675
|
-
|
|
1717
|
+
const workerCmd = resolveClaudeSpawnCommand('worker', '--dangerously-skip-permissions --dangerously-load-development-channels server:wogi-workspace-channel');
|
|
1718
|
+
execSync(workerCmd, {
|
|
1676
1719
|
cwd,
|
|
1677
1720
|
env,
|
|
1678
1721
|
stdio: 'inherit'
|
package/package.json
CHANGED
package/scripts/flow
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - `flow ask` CLI
|
|
5
|
+
*
|
|
6
|
+
* Mark that the AI has a pending question for the user. Suppresses the
|
|
7
|
+
* task-boundary session restart (wf-39e9dc09) until the user responds — so
|
|
8
|
+
* the AI can complete a task AND ask a follow-up without losing the question
|
|
9
|
+
* across the restart boundary.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* flow ask "<question text>"
|
|
13
|
+
*
|
|
14
|
+
* Behavior:
|
|
15
|
+
* Writes `.workflow/state/pending-question.json` with the question + timestamp.
|
|
16
|
+
* The Stop hook's restart logic checks this file — if present, restart is deferred.
|
|
17
|
+
* UserPromptSubmit clears the file when the user responds.
|
|
18
|
+
*
|
|
19
|
+
* When to call:
|
|
20
|
+
* Any time the AI asks the user a question during or after a task. Especially
|
|
21
|
+
* safe to call even when unsure — worst case, it just delays a restart by one
|
|
22
|
+
* turn. Under-calling is the risk (orphaned question after restart).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const { PATHS } = require('./flow-utils');
|
|
29
|
+
|
|
30
|
+
const PENDING_QUESTION_FILE = 'pending-question.json';
|
|
31
|
+
|
|
32
|
+
function getPendingQuestionPath() {
|
|
33
|
+
return path.join(PATHS.state, PENDING_QUESTION_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Write the pending-question marker.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} question — question text (1-1000 chars)
|
|
40
|
+
* @returns {{ marked: boolean, path?: string, reason?: string }}
|
|
41
|
+
*/
|
|
42
|
+
function markQuestionPending(question) {
|
|
43
|
+
if (typeof question !== 'string' || question.trim().length === 0) {
|
|
44
|
+
return { marked: false, reason: 'empty-question' };
|
|
45
|
+
}
|
|
46
|
+
const trimmed = question.trim().slice(0, 1000);
|
|
47
|
+
try {
|
|
48
|
+
const p = getPendingQuestionPath();
|
|
49
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
50
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
51
|
+
version: 1,
|
|
52
|
+
question: trimmed,
|
|
53
|
+
askedAt: new Date().toISOString()
|
|
54
|
+
}, null, 2));
|
|
55
|
+
return { marked: true, path: p };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { marked: false, reason: `write-failed: ${err.message}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear the pending-question marker (called from UserPromptSubmit).
|
|
63
|
+
*
|
|
64
|
+
* @returns {{ cleared: boolean, wasPresent: boolean }}
|
|
65
|
+
*/
|
|
66
|
+
function clearPendingQuestion() {
|
|
67
|
+
const p = getPendingQuestionPath();
|
|
68
|
+
if (!fs.existsSync(p)) return { cleared: true, wasPresent: false };
|
|
69
|
+
try {
|
|
70
|
+
fs.unlinkSync(p);
|
|
71
|
+
return { cleared: true, wasPresent: true };
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return { cleared: false, wasPresent: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check whether a pending-question marker exists.
|
|
79
|
+
*
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function hasPendingQuestion() {
|
|
83
|
+
try { return fs.existsSync(getPendingQuestionPath()); } catch (_e) { return false; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
markQuestionPending,
|
|
88
|
+
clearPendingQuestion,
|
|
89
|
+
hasPendingQuestion,
|
|
90
|
+
getPendingQuestionPath
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// CLI entry
|
|
94
|
+
if (require.main === module) {
|
|
95
|
+
const args = process.argv.slice(2);
|
|
96
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
97
|
+
console.log('Usage: flow ask "<question text>"');
|
|
98
|
+
console.log('Marks a pending question; the restart mechanism will defer until');
|
|
99
|
+
console.log('the user responds (UserPromptSubmit clears the flag).');
|
|
100
|
+
process.exit(args.length === 0 ? 2 : 0);
|
|
101
|
+
}
|
|
102
|
+
if (args[0] === '--clear') {
|
|
103
|
+
const r = clearPendingQuestion();
|
|
104
|
+
console.log(JSON.stringify(r));
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
if (args[0] === '--check') {
|
|
108
|
+
console.log(JSON.stringify({ hasPendingQuestion: hasPendingQuestion() }));
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
const question = args.join(' ');
|
|
112
|
+
const r = markQuestionPending(question);
|
|
113
|
+
if (r.marked) {
|
|
114
|
+
console.log(`Pending question marked. Restart deferred until you respond.`);
|
|
115
|
+
console.log(` File: ${r.path}`);
|
|
116
|
+
process.exit(0);
|
|
117
|
+
} else {
|
|
118
|
+
console.error(`Failed to mark: ${r.reason}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -850,6 +850,16 @@ const CONFIG_DEFAULTS = {
|
|
|
850
850
|
respectDependencies: true
|
|
851
851
|
},
|
|
852
852
|
|
|
853
|
+
// --- Session Hydration (wf-729ab5c0) ---
|
|
854
|
+
// Controls how much session-episodic content (request-log entries, recent
|
|
855
|
+
// activity) gets injected into SessionStart's additionalContext. Rule-class
|
|
856
|
+
// files (decisions.md, app-map, etc.) are NOT affected — rules don't expire.
|
|
857
|
+
sessionHydration: {
|
|
858
|
+
_comment: 'Recency-based filter for SessionStart episodic-content injection. Complements wf-39e9dc09 task-boundary restart.',
|
|
859
|
+
recencyWindowHours: 48,
|
|
860
|
+
_comment_recencyWindowHours: 'Session-episodic entries older than this are excluded from hydration (still on disk, loadable via Read/Grep on demand). 0 = disable time filter (count-based limits still apply).'
|
|
861
|
+
},
|
|
862
|
+
|
|
853
863
|
// --- Task-Boundary Session Restart (wf-39e9dc09) ---
|
|
854
864
|
// EXPERIMENTAL, OPT-IN. When enabled AND the `wogi-claude` wrapper is running,
|
|
855
865
|
// TaskCompleted triggers a clean restart of the Claude Code process so each
|
|
@@ -144,7 +144,9 @@ const KNOWN_CONFIG_KEYS = [
|
|
|
144
144
|
// v2.0.0+ compat shim output keys
|
|
145
145
|
'proactiveCompaction', 'communitySync',
|
|
146
146
|
// Task-boundary session restart (wf-39e9dc09) — opt-in, experimental
|
|
147
|
-
'taskBoundaryReset'
|
|
147
|
+
'taskBoundaryReset',
|
|
148
|
+
// Session hydration recency filter (wf-729ab5c0)
|
|
149
|
+
'sessionHydration'
|
|
148
150
|
];
|
|
149
151
|
|
|
150
152
|
module.exports = {
|
|
@@ -398,11 +398,33 @@ function getKeyDecisions(maxEntries = 5) {
|
|
|
398
398
|
* @param {number} maxEntries - Max entries to return
|
|
399
399
|
* @returns {Array} Recent activity
|
|
400
400
|
*/
|
|
401
|
+
/**
|
|
402
|
+
* Get recency cutoff Date from config (wf-729ab5c0).
|
|
403
|
+
*
|
|
404
|
+
* Returns a Date object representing the boundary: entries with dates BEFORE
|
|
405
|
+
* this are considered "stale" for session-episodic hydration. Returns null
|
|
406
|
+
* when time filtering is disabled (recencyWindowHours <= 0).
|
|
407
|
+
*
|
|
408
|
+
* @returns {Date|null}
|
|
409
|
+
*/
|
|
410
|
+
function getRecencyCutoff() {
|
|
411
|
+
try {
|
|
412
|
+
const config = getConfig();
|
|
413
|
+
const hours = config.sessionHydration?.recencyWindowHours;
|
|
414
|
+
if (typeof hours !== 'number' || hours <= 0) return null;
|
|
415
|
+
return new Date(Date.now() - hours * 3600 * 1000);
|
|
416
|
+
} catch (_err) {
|
|
417
|
+
return null; // Safe degrade: no time filter if config read fails
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
401
421
|
function getRecentActivity(maxEntries = 3) {
|
|
402
422
|
if (!fs.existsSync(PATHS.requestLog)) {
|
|
403
423
|
return [];
|
|
404
424
|
}
|
|
405
425
|
|
|
426
|
+
const recencyCutoff = getRecencyCutoff();
|
|
427
|
+
|
|
406
428
|
try {
|
|
407
429
|
// Wrap in try-catch per security-patterns.md Rule #1
|
|
408
430
|
// Race conditions/permission changes can cause fs.readFileSync to fail even after existsSync
|
|
@@ -433,6 +455,18 @@ function getRecentActivity(maxEntries = 3) {
|
|
|
433
455
|
if (!headerMatch) continue;
|
|
434
456
|
|
|
435
457
|
const id = `R-${headerMatch[1]}`;
|
|
458
|
+
const dateStr = headerMatch[2];
|
|
459
|
+
|
|
460
|
+
// wf-729ab5c0 — recency filter.
|
|
461
|
+
// If a cutoff is configured AND this entry's date is parseable AND it
|
|
462
|
+
// predates the cutoff, skip it. If the date is unparseable, INCLUDE the
|
|
463
|
+
// entry (safe default: don't drop content we can't classify).
|
|
464
|
+
if (recencyCutoff) {
|
|
465
|
+
const entryDate = new Date(dateStr);
|
|
466
|
+
if (!isNaN(entryDate.getTime()) && entryDate < recencyCutoff) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
436
470
|
|
|
437
471
|
// Extract request line
|
|
438
472
|
// Length-capped capture to prevent ReDoS on crafted single-line entries
|
|
@@ -1108,6 +1142,7 @@ module.exports = {
|
|
|
1108
1142
|
getPendingTaskSummary,
|
|
1109
1143
|
getKeyDecisions,
|
|
1110
1144
|
getRecentActivity,
|
|
1145
|
+
getRecencyCutoff,
|
|
1111
1146
|
getSessionState,
|
|
1112
1147
|
gatherSessionContext,
|
|
1113
1148
|
formatContextForInjection
|
|
@@ -136,6 +136,16 @@ function consumeAndTriggerRestart() {
|
|
|
136
136
|
return { triggered: false, reason: 'no-pending-marker' };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Defer restart when the AI has a pending question for the user
|
|
140
|
+
// (wf-729ab5c0 follow-up / pending-question safety).
|
|
141
|
+
// The marker STAYS — we'll try again next Stop hook after user responds.
|
|
142
|
+
try {
|
|
143
|
+
const { hasPendingQuestion } = require('../../flow-ask');
|
|
144
|
+
if (hasPendingQuestion()) {
|
|
145
|
+
return { triggered: false, reason: 'pending-question-deferred' };
|
|
146
|
+
}
|
|
147
|
+
} catch (_err) { /* flow-ask may not be present in older installs; degrade open */ }
|
|
148
|
+
|
|
139
149
|
const pre = checkPreconditions();
|
|
140
150
|
if (!pre.ready) {
|
|
141
151
|
if (process.env.DEBUG) {
|
|
@@ -27,6 +27,18 @@ runHook('UserPromptSubmit', async ({ input, parsedInput }) => {
|
|
|
27
27
|
const prompt = parsedInput.prompt;
|
|
28
28
|
const source = parsedInput.source;
|
|
29
29
|
|
|
30
|
+
// wf-729ab5c0 follow-up — clear pending-question marker on user response.
|
|
31
|
+
// This unblocks a deferred task-boundary restart. The AI may have asked a
|
|
32
|
+
// question via `flow ask "..."` after task completion; user's response
|
|
33
|
+
// releases the deferral, and the next Stop hook will fire the restart.
|
|
34
|
+
try {
|
|
35
|
+
const { clearPendingQuestion } = require('../../../flow-ask');
|
|
36
|
+
const r = clearPendingQuestion();
|
|
37
|
+
if (r.wasPresent && process.env.DEBUG) {
|
|
38
|
+
console.error(`[UserPromptSubmit] Cleared pending-question marker — restart deferral released`);
|
|
39
|
+
}
|
|
40
|
+
} catch (_err) { /* non-fatal */ }
|
|
41
|
+
|
|
30
42
|
// v4.1: Detect skill commands that need execution tracking
|
|
31
43
|
if (typeof prompt === 'string') {
|
|
32
44
|
const skillMatch = prompt.match(/^\/(wogi-bulk|wogi-start)\b/i);
|