wogiflow 2.26.2 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +84 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-context-compact/expander.js +1 -1
  70. package/scripts/flow-context-compact/section-extractor.js +2 -2
  71. package/scripts/flow-context-gatherer.js +1 -1
  72. package/scripts/flow-context-generator.js +1 -1
  73. package/scripts/flow-context-scoring.js +1 -1
  74. package/scripts/flow-correct.js +1 -1
  75. package/scripts/flow-decision-authority.js +66 -15
  76. package/scripts/flow-done.js +33 -1
  77. package/scripts/flow-epic-cascade.js +171 -0
  78. package/scripts/flow-epics.js +2 -7
  79. package/scripts/flow-eval-judge.js +1 -1
  80. package/scripts/flow-eval.js +1 -1
  81. package/scripts/flow-export-scanner.js +2 -6
  82. package/scripts/flow-failure-learning.js +1 -1
  83. package/scripts/flow-feature-dossier.js +787 -0
  84. package/scripts/flow-figma-extract.js +2 -2
  85. package/scripts/flow-figma-generate.js +1 -1
  86. package/scripts/flow-gate-confidence.js +1 -1
  87. package/scripts/flow-health.js +52 -1
  88. package/scripts/flow-hooks.js +1 -1
  89. package/scripts/flow-id.js +19 -3
  90. package/scripts/flow-instruction-richness.js +1 -1
  91. package/scripts/flow-knowledge-router.js +1 -1
  92. package/scripts/flow-knowledge-sync.js +1 -1
  93. package/scripts/flow-logic-adversary.js +76 -1
  94. package/scripts/flow-logic-rules.js +380 -0
  95. package/scripts/flow-long-input.js +5 -5
  96. package/scripts/flow-memory-sync.js +1 -1
  97. package/scripts/flow-memory.js +78 -7
  98. package/scripts/flow-migrate.js +1 -1
  99. package/scripts/flow-model-caller.js +1 -1
  100. package/scripts/flow-models.js +2 -2
  101. package/scripts/flow-morning.js +0 -17
  102. package/scripts/flow-multi-approach.js +1 -1
  103. package/scripts/flow-orchestrate-context.js +4 -4
  104. package/scripts/flow-orchestrate-templates.js +1 -1
  105. package/scripts/flow-orchestrate.js +8 -8
  106. package/scripts/flow-peer-review.js +1 -1
  107. package/scripts/flow-phase.js +9 -0
  108. package/scripts/flow-proactive-compact.js +1 -1
  109. package/scripts/flow-providers.js +1 -1
  110. package/scripts/flow-question-queue.js +255 -0
  111. package/scripts/flow-repo-map.js +312 -0
  112. package/scripts/flow-review-passes/index.js +1 -1
  113. package/scripts/flow-review-passes/integration.js +1 -1
  114. package/scripts/flow-review-passes/structure.js +1 -1
  115. package/scripts/flow-revision-tracker.js +1 -1
  116. package/scripts/flow-section-resolver.js +1 -1
  117. package/scripts/flow-session-end.js +74 -5
  118. package/scripts/flow-session-state.js +103 -1
  119. package/scripts/flow-setup-hooks.js +1 -1
  120. package/scripts/flow-skeptical-evaluator.js +274 -0
  121. package/scripts/flow-skill-generator.js +3 -3
  122. package/scripts/flow-skill-learn.js +3 -6
  123. package/scripts/flow-skill-manage.js +248 -0
  124. package/scripts/flow-spec-verifier.js +1 -1
  125. package/scripts/flow-standards-checker.js +75 -0
  126. package/scripts/flow-standards-gate.js +1 -1
  127. package/scripts/flow-statusline-setup.js +8 -2
  128. package/scripts/flow-step-changelog.js +2 -2
  129. package/scripts/flow-step-coverage.js +1 -1
  130. package/scripts/flow-step-knowledge.js +1 -1
  131. package/scripts/flow-step-regression.js +1 -1
  132. package/scripts/flow-step-simplifier.js +1 -1
  133. package/scripts/flow-task-analyzer.js +1 -1
  134. package/scripts/flow-task-classifier.js +1 -1
  135. package/scripts/flow-task-enforcer.js +1 -1
  136. package/scripts/flow-template-extractor.js +1 -1
  137. package/scripts/flow-trap-zone.js +1 -1
  138. package/scripts/flow-utils.js +4 -0
  139. package/scripts/flow-worker-question-classifier.js +51 -5
  140. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  141. package/scripts/flow-workspace-summary.js +256 -0
  142. package/scripts/hooks/adapters/base-adapter.js +2 -2
  143. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  144. package/scripts/hooks/core/observation-capture.js +24 -0
  145. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  146. package/scripts/hooks/core/phase-gate.js +15 -1
  147. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  148. package/scripts/hooks/core/post-compact.js +5 -2
  149. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  150. package/scripts/hooks/core/routing-gate.js +58 -0
  151. package/scripts/hooks/core/session-context.js +108 -0
  152. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  153. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  154. package/scripts/hooks/core/session-end.js +25 -0
  155. package/scripts/hooks/core/setup-handler.js +1 -1
  156. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  157. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  158. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  159. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  160. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  161. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  162. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  163. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  164. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -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 };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Flow — Session End: Skill Proposal Surfacing
5
+ *
6
+ * Reads pending skill proposals staged by `flow skill propose|patch|remove`
7
+ * and returns a structured summary for the session-end adapter to display.
8
+ *
9
+ * Agent-proposed changes do NOT auto-apply. The user reviews at session-end
10
+ * and runs `flow skill promote <name>` / `flow skill reject <name>`.
11
+ */
12
+
13
+ const store = require('../../../lib/skill-proposal-store');
14
+
15
+ function summarizePendingProposals() {
16
+ let pending;
17
+ try {
18
+ pending = store.listProposals({ status: 'pending' });
19
+ } catch (_err) {
20
+ return null;
21
+ }
22
+ if (!pending || pending.length === 0) return null;
23
+
24
+ const byAction = { propose: 0, patch: 0, remove: 0 };
25
+ const lines = [];
26
+ for (const p of pending) {
27
+ byAction[p.action] = (byAction[p.action] || 0) + 1;
28
+ const icon = p.action === 'propose' ? '+' : p.action === 'patch' ? '~' : '-';
29
+ const rationale = p.rationale ? ` — ${p.rationale}` : '';
30
+ lines.push(` ${icon} ${p.skillName} (${p.action}, ${p.id})${rationale}`);
31
+ }
32
+
33
+ return {
34
+ count: pending.length,
35
+ byAction,
36
+ proposals: pending,
37
+ lines,
38
+ message: formatMessage(pending.length, byAction, lines),
39
+ };
40
+ }
41
+
42
+ function formatMessage(count, byAction, lines) {
43
+ const plural = count !== 1 ? 's' : '';
44
+ const breakdown = Object.entries(byAction)
45
+ .filter(([, n]) => n > 0)
46
+ .map(([a, n]) => `${n} ${a}`)
47
+ .join(', ');
48
+ return [
49
+ `${count} pending skill proposal${plural} (${breakdown}):`,
50
+ ...lines,
51
+ '',
52
+ 'Review with: flow skill pending',
53
+ 'Approve: flow skill promote <name>',
54
+ 'Discard: flow skill reject <name>',
55
+ ].join('\n');
56
+ }
57
+
58
+ module.exports = { summarizePendingProposals };
@@ -68,6 +68,31 @@ function handleSessionEnd(input) {
68
68
  result.logged = false;
69
69
  }
70
70
 
71
+ // Surface pending skill proposals staged by `flow skill propose|patch|remove`.
72
+ // These await user approval (`flow skill promote|reject`) at session end.
73
+ try {
74
+ const { summarizePendingProposals } = require('./session-end-skill-proposals');
75
+ const summary = summarizePendingProposals();
76
+ if (summary) {
77
+ result.pendingSkillProposals = summary;
78
+ }
79
+ } catch (_err) {
80
+ // Non-critical — never block session end
81
+ }
82
+
83
+ // Surface pending IGR-artifact memory proposals staged by `flow memory
84
+ // propose`. These await user approval (`flow memory approve|reject`) at
85
+ // session end. Story: wf-4434851f.
86
+ try {
87
+ const { summarizePendingMemoryProposals } = require('./session-end-memory-proposals');
88
+ const summary = summarizePendingMemoryProposals();
89
+ if (summary) {
90
+ result.pendingMemoryProposals = summary;
91
+ }
92
+ } catch (_err) {
93
+ // Non-critical — never block session end
94
+ }
95
+
71
96
  // Scratch directory cleanup — remove temp files created during session
72
97
  try {
73
98
  const fs = require('node:fs');
@@ -45,7 +45,7 @@ function getSetupConfig() {
45
45
  * @returns {Object} Result: { needsSetup, message, action, context }
46
46
  */
47
47
  function handleSetup(options = {}) {
48
- const { trigger = 'init' } = options;
48
+ const { _trigger = 'init' } = options;
49
49
 
50
50
  if (!isSetupEnabled()) {
51
51
  return {
@@ -51,6 +51,7 @@ const { safeJsonParse } = require('../../flow-io');
51
51
 
52
52
  const PENDING_MARKER_FILE = 'task-just-completed';
53
53
  const LAST_TRIGGERED_FILE = 'task-boundary-last-triggered';
54
+ const CLEAN_COMPLETION_MARKER_FILE = 'task-boundary-clean-completion.json';
54
55
  // Window during which a recentlyCompleted[0] entry is considered "fresh
55
56
  // enough" to retro-mark Phase 1 from the Stop hook. Large enough to cover
56
57
  // a slow quality-gate run; small enough that a session opened hours later
@@ -69,6 +70,56 @@ function getLastTriggeredPath() {
69
70
  return path.join(PATHS.state, LAST_TRIGGERED_FILE);
70
71
  }
71
72
 
73
+ function getCleanCompletionMarkerPath() {
74
+ return path.join(PATHS.state, CLEAN_COMPLETION_MARKER_FILE);
75
+ }
76
+
77
+ /**
78
+ * Write the clean-completion marker when Phase 2 successfully restarts after a
79
+ * cleanly-completed task. The next session's SessionStart hook reads this
80
+ * marker to decide whether to inject an AUTO-PICKUP block (wf-f267ea2a).
81
+ *
82
+ * Best-effort — failure here does NOT abort the restart; auto-pickup just
83
+ * won't fire for this restart.
84
+ */
85
+ function writeCleanCompletionMarker(taskId, taskTitle, options = {}) {
86
+ // Durable write (Story E / wf-e28b6cd8 — Blocker M2 fix).
87
+ //
88
+ // Original failure mode: writeFileSync schedules the data; the kernel buffers
89
+ // it; SIGTERM is dispatched; the process exits before the buffer reaches
90
+ // disk. The restarted session sees no marker → cascade silently fails.
91
+ //
92
+ // Fix: write to a tmp file, fsync the FILE, atomic-rename to final, fsync
93
+ // the DIRECTORY. The directory fsync is what guarantees the rename itself
94
+ // is durable across the SIGTERM/relaunch boundary.
95
+ try {
96
+ const p = getCleanCompletionMarkerPath();
97
+ const dir = path.dirname(p);
98
+ fs.mkdirSync(dir, { recursive: true });
99
+ const payload = {
100
+ version: 1,
101
+ completedTaskId: taskId || null,
102
+ completedTaskTitle: taskTitle || null,
103
+ completedAt: new Date().toISOString(),
104
+ ...(options.nextTaskId ? { nextTaskId: options.nextTaskId } : {})
105
+ };
106
+ const data = JSON.stringify(payload);
107
+ const tmp = `${p}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
108
+ const fd = fs.openSync(tmp, 'w');
109
+ try {
110
+ fs.writeSync(fd, data);
111
+ fs.fsyncSync(fd);
112
+ } finally {
113
+ fs.closeSync(fd);
114
+ }
115
+ fs.renameSync(tmp, p);
116
+ try {
117
+ const dfd = fs.openSync(dir, 'r');
118
+ try { fs.fsyncSync(dfd); } finally { fs.closeSync(dfd); }
119
+ } catch (_err) { /* directory fsync is best-effort */ }
120
+ } catch (_err) { /* best effort — auto-pickup just won't fire if this fails */ }
121
+ }
122
+
72
123
  function readLastTriggered() {
73
124
  try {
74
125
  return safeJsonParse(getLastTriggeredPath(), null);
@@ -155,9 +206,13 @@ function checkPreconditions() {
155
206
  * Returns a result object for diagnostics; never throws. If something goes
156
207
  * wrong, the Stop hook should continue with its normal flow.
157
208
  *
158
- * @returns {{ triggered: boolean, reason?: string, flagPath?: string, parentPid?: number }}
209
+ * @param {Object} [opts]
210
+ * @param {string} [opts.transcriptPath] - Claude Code transcript path, used by
211
+ * the main-mode question classifier safety net. When absent, the classifier
212
+ * step is skipped (fail-open).
213
+ * @returns {Promise<{ triggered: boolean, reason?: string, flagPath?: string, parentPid?: number }>}
159
214
  */
160
- function consumeAndTriggerRestart() {
215
+ async function consumeAndTriggerRestart(opts = {}) {
161
216
  const markerPath = getPendingMarkerPath();
162
217
  if (!fs.existsSync(markerPath)) {
163
218
  return { triggered: false, reason: 'no-pending-marker' };
@@ -173,6 +228,42 @@ function consumeAndTriggerRestart() {
173
228
  }
174
229
  } catch (_err) { /* flow-ask may not be present in older installs; degrade open */ }
175
230
 
231
+ // Main-mode question classifier safety net (wf-191d5f6e). Catches the case
232
+ // where the AI ends a turn with an open user-facing question but forgot to
233
+ // call `flow ask` first. On a YES classification, auto-write the
234
+ // pending-question marker and defer the restart. Fail-open throughout —
235
+ // any error or skip falls through to normal restart logic.
236
+ try {
237
+ const isWorker = process.env.WOGI_WORKSPACE_ROOT &&
238
+ process.env.WOGI_REPO_NAME &&
239
+ process.env.WOGI_REPO_NAME !== 'manager';
240
+ if (!isWorker) {
241
+ const config = getConfig();
242
+ const clf = config.mainModeQuestionClassifier;
243
+ const enabled = clf?.enabled !== false; // default true
244
+ if (enabled && opts.transcriptPath) {
245
+ const { classifyQuestion } = require('../../flow-worker-question-classifier');
246
+ const result = await classifyQuestion({
247
+ mode: 'main',
248
+ transcriptPath: opts.transcriptPath,
249
+ minConfidence: Number.isFinite(clf?.minConfidence) ? clf.minConfidence : 70,
250
+ model: typeof clf?.model === 'string' ? clf.model : undefined
251
+ });
252
+ if (result?.blocked) {
253
+ try {
254
+ const { markQuestionPending } = require('../../flow-ask');
255
+ markQuestionPending(`auto-deferred: ${String(result.reason || 'classifier detected open question').slice(0, 500)}`);
256
+ } catch (_err) { /* best effort — marker write failure falls through to restart */ }
257
+ return { triggered: false, reason: 'auto-deferred-question-detected' };
258
+ }
259
+ }
260
+ }
261
+ } catch (err) {
262
+ if (process.env.DEBUG) {
263
+ console.error(`[task-boundary-reset] main-mode classifier error (fail-open): ${err.message}`);
264
+ }
265
+ }
266
+
176
267
  const pre = checkPreconditions();
177
268
  if (!pre.ready) {
178
269
  if (process.env.DEBUG) {
@@ -210,6 +301,13 @@ function consumeAndTriggerRestart() {
210
301
  return { triggered: false, reason: `flag-write-failed: ${err.message}` };
211
302
  }
212
303
 
304
+ // Write the clean-completion marker so the NEXT session's SessionStart
305
+ // hook can inject the AUTO-PICKUP block (wf-f267ea2a). This is gated on
306
+ // config.taskBoundaryReset.autoPickupNextTask in session-context.js — the
307
+ // marker is always written here when Phase 2 fires (cheap, harmless), and
308
+ // session-context decides whether to act on it.
309
+ writeCleanCompletionMarker(markerPayload?.taskId, markerPayload?.taskTitle);
310
+
213
311
  // SIGTERM our parent (claude). The wrapper sees the flag on claude's exit
214
312
  // and restarts. If SIGTERM turns out to not shut claude down cleanly in
215
313
  // real testing, try SIGHUP or SIGINT as fallbacks (see spec wf-39e9dc09).
@@ -329,6 +427,8 @@ module.exports = {
329
427
  checkPreconditions,
330
428
  hasPendingMarker,
331
429
  getPendingMarkerPath,
430
+ getCleanCompletionMarkerPath,
431
+ writeCleanCompletionMarker,
332
432
 
333
433
  // Back-compat: earlier code calls this name. Route it to Phase 1 so existing
334
434
  // wiring in task-completed.js still does the right thing (mark the marker,
@@ -352,8 +452,14 @@ if (require.main === module) {
352
452
  process.exit(0);
353
453
  }
354
454
  if (arg === 'consume') {
355
- console.log(JSON.stringify(consumeAndTriggerRestart(), null, 2));
356
- process.exit(0);
455
+ consumeAndTriggerRestart().then((r) => {
456
+ console.log(JSON.stringify(r, null, 2));
457
+ process.exit(0);
458
+ }).catch((err) => {
459
+ console.error(err.message);
460
+ process.exit(1);
461
+ });
462
+ return;
357
463
  }
358
464
  console.log('Usage: node task-boundary-reset.js <check|has-pending|mark|consume>');
359
465
  process.exit(2);
@@ -99,7 +99,78 @@ function isWorkspaceWorker() {
99
99
  return true;
100
100
  }
101
101
 
102
+ /**
103
+ * Path-discipline gate (Story B / wf-ab59f0e4 — Phase 4.5).
104
+ *
105
+ * Single-writer invariant: workers NEVER write into the workspace-manager
106
+ * tree (`<workspace>/.workspace/**`); the manager NEVER writes into worker
107
+ * member-repo `.workflow/state/**` paths. Cross-process state coordination
108
+ * happens exclusively via the channel-dispatch HTTP bus.
109
+ *
110
+ * Without this check, a confused worker (or hostile prompt-injection
111
+ * payload that talked the worker into editing manager state) could corrupt
112
+ * `dispatched-tasks.json` for ALL workers in the workspace. The check
113
+ * fails LOUD (block + clear error) so the boundary violation is impossible
114
+ * to ignore — silent corruption is the worst-case alternative.
115
+ *
116
+ * Returns the same `{ blocked, reason?, message? }` shape as
117
+ * `checkWorkerBoundary` so the caller can compose the two checks.
118
+ *
119
+ * @param {string} toolName
120
+ * @param {Object} toolInput - { file_path } for Edit/Write
121
+ * @returns {{ blocked: boolean, reason?: string, message?: string }}
122
+ */
123
+ function checkPathDiscipline(toolName, toolInput) {
124
+ if (!process.env.WOGI_WORKSPACE_ROOT) return { blocked: false };
125
+ const writeTools = new Set(['Edit', 'Write', 'NotebookEdit']);
126
+ if (!writeTools.has(toolName)) return { blocked: false };
127
+ const filePath = toolInput && (toolInput.file_path || toolInput.notebook_path);
128
+ if (typeof filePath !== 'string' || !filePath) return { blocked: false };
129
+
130
+ const repo = process.env.WOGI_REPO_NAME || '';
131
+ const root = process.env.WOGI_WORKSPACE_ROOT;
132
+ const managerStateDir = `${root.replace(/\/+$/, '')}/.workspace/`;
133
+ const memberStateDirRegex = /\/members?\/[^/]+\/\.workflow\/state\//;
134
+
135
+ if (repo && repo !== 'manager') {
136
+ if (filePath.startsWith(managerStateDir)) {
137
+ return {
138
+ blocked: true,
139
+ reason: 'path-discipline-worker',
140
+ message: [
141
+ `PATH DISCIPLINE: workers MUST NOT write to manager-owned files.`,
142
+ ``,
143
+ `Blocked: ${filePath}`,
144
+ ``,
145
+ `${managerStateDir}** is owned by the manager process.`,
146
+ `Use the channel-dispatch HTTP bus to communicate with the manager;`,
147
+ `never edit shared workspace state directly. See Story B (wf-ab59f0e4)`,
148
+ `Phase 4.5 in .workflow/changes/wf-ab59f0e4.md.`
149
+ ].join('\n')
150
+ };
151
+ }
152
+ }
153
+
154
+ if (repo === 'manager' && memberStateDirRegex.test(filePath)) {
155
+ return {
156
+ blocked: true,
157
+ reason: 'path-discipline-manager',
158
+ message: [
159
+ `PATH DISCIPLINE: manager MUST NOT write to worker member-repo state.`,
160
+ ``,
161
+ `Blocked: ${filePath}`,
162
+ ``,
163
+ `Worker member-repos own their own .workflow/state/. Send a channel`,
164
+ `dispatch to the worker if state changes are needed there.`
165
+ ].join('\n')
166
+ };
167
+ }
168
+
169
+ return { blocked: false };
170
+ }
171
+
102
172
  module.exports = {
103
173
  checkWorkerBoundary,
174
+ checkPathDiscipline,
104
175
  isWorkspaceWorker
105
176
  };