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.
@@ -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
- execSync('claude --dangerously-skip-permissions --dangerously-load-development-channels server:wogi-workspace-channel', {
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
- execSync('claude --dangerously-skip-permissions --dangerously-load-development-channels server:wogi-workspace-channel', {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/scripts/flow CHANGED
@@ -516,6 +516,9 @@ case "${1:-}" in
516
516
  correct)
517
517
  node "$SCRIPT_DIR/flow-correct.js" "${@:2}"
518
518
  ;;
519
+ ask)
520
+ node "$SCRIPT_DIR/flow-ask.js" "${@:2}"
521
+ ;;
519
522
  aggregate)
520
523
  node "$SCRIPT_DIR/flow-aggregate.js" "${@:2}"
521
524
  ;;
@@ -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);