wogiflow 2.25.1 → 2.26.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.
@@ -13,7 +13,7 @@ Steps:
13
13
  6. **Completion-claim honesty scan** - Surface done-in-text-but-not-in-status contradictions (2026-04-16 honesty-infra)
14
14
  7. **Workspace session-end message (v2.23.0+)** - If running inside a workspace manager, write a `heads-up` message to `.workspace/messages/` so workers know no new dispatches are coming
15
15
  8. **Commit changes** - Stage and commit all workflow files
16
- 9. **Offer to push** - Ask if should push to remote
16
+ 9. **Offer to push** - Ask if should push to remote. In workspace worker mode, the prompt is suppressed (workers cannot prompt the user directly). When `config.sessionEnd.autoPushInWorker` is `true` (default), the worker auto-pushes silently. When `false`, the push is skipped and the user pushes manually later. Non-worker sessions are unchanged.
17
17
 
18
18
  Output:
19
19
  ```
@@ -33,7 +33,7 @@ Reflection: "Have I introduced any bugs or regressions?"
33
33
 
34
34
  1. Reflection: "Does this match what the user asked for?"
35
35
  2. Close out all TodoWrite items for this task
36
- 3. Move task to recentlyCompleted in ready.json
36
+ 3. **Run `node node_modules/wogiflow/scripts/flow-done.js <taskId>`** — this is the ONLY supported way to complete a task. It runs quality gates, moves the task from `inProgress` → `recentlyCompleted`, writes the gate latch, and fires the task-boundary-restart Phase 1 marker. **Do NOT hand-edit `ready.json` to move the task** — that bypasses the CLI and silently disables: quality-gate verification, gate latch, and the task-boundary session restart. If `flow` is not on PATH in this environment, invoke it as `node node_modules/wogiflow/scripts/flow-done.js <taskId>` directly.
37
37
  4. Registry maps auto-updated by `registryUpdate` quality gate (runs `flow registry-manager scan` on all active registries — app-map, function-map, api-map, schema-map, service-map)
38
38
  5. If `config.webmcp.enabled` and UI files created: run `node node_modules/wogiflow/scripts/flow-webmcp-generator.js scan`
39
39
  6. Commit: `feat: Complete wf-XXXXXXXX - [title]`
@@ -432,6 +432,10 @@ Use `/wogi-research "question"` for rigorous verification.
432
432
 
433
433
  ---
434
434
 
435
+ {{> methodology-rules}}
436
+
437
+ ---
438
+
435
439
  ## Generated by CLI Bridge
436
440
 
437
441
  This file was generated by the Wogi Flow CLI bridge.
@@ -0,0 +1,149 @@
1
+ ## WogiFlow Methodology Rules
2
+
3
+ These are product-level rules that apply to every WogiFlow session. They ship with the tool — enforcement is in the shipped scripts/hooks, and the text below explains the contract to Claude so it doesn't try to work around the enforcement.
4
+
5
+ ---
6
+
7
+ ### Research Before Propose (MANDATORY)
8
+
9
+ **Rule**: Before proposing any fix, plan, or spec, audit existing infrastructure for the problem area. Propose only what fills a confirmed gap. Evidence-before-invention.
10
+
11
+ **What counts as research**: read relevant files in `.workflow/state/` (decisions.md, feedback-patterns.md, app-map.md, function-map.md, api-map.md), read the task spec from `.workflow/changes/` or `.workflow/specs/`, grep existing hooks/classifiers/gates, read relevant source files.
12
+
13
+ **Why**: baseline LLM training biases toward generating plausible-sounding solutions. In a codebase with existing infrastructure, "plausible" is frequently wrong — proposing a feature that already exists, missing an existing pattern, or reinventing a wired-up hook. The correction cycle cost (user rejecting → replanning → rejecting again) is higher than the upfront audit cost.
14
+
15
+ **You MAY ask the user clarifying questions when genuinely needed.** The rule is not "never ask" — it is "don't propose before researching." Asking is a valid escape hatch; proposing without evidence is not.
16
+
17
+ **Enforcement**: `scripts/hooks/core/research-evidence-gate.js` tracks state-file reads (`.workflow/state/`, `.workflow/changes/`, `.workflow/specs/`, `.workflow/epics/`) in the current task turn. Three enforcement points check the evidence fingerprint before proposal actions:
18
+
19
+ 1. **Phase transition** — `transitionPhase()` blocks `→ spec_review` and `→ coding` until `minEvidence` distinct state/spec file reads have been recorded.
20
+ 2. **Spec write** — PreToolUse blocks `Edit`/`Write` to `.workflow/changes/*.md`, `.workflow/specs/*.md`, or `.workflow/epics/*.md` when evidence is below threshold.
21
+ 3. **Channel dispatch** — in workspace manager mode, `dispatchToChannel()` blocks dispatching a task to a worker until the manager has read evidence from the target member repo.
22
+
23
+ Evidence is cleared at task start, session end, and post-compaction so each task begins with a clean slate. The `AskUserQuestion` tool is NOT gated — asking for clarification is a valid escape hatch. IGR's architect + adversary passes challenge solution *quality* downstream; this gate enforces the evidence *base* upstream.
24
+
25
+ **Config**: `hooks.rules.researchEvidenceGate.{enabled,minEvidence}` (defaults: `true`, `2`).
26
+
27
+ ---
28
+
29
+ ### Completion-Claim Honesty Scan
30
+
31
+ **Rule**: At session-end and on `flow health`, scan `ready.json` entries for two contradiction classes and surface (not block) them for user reconciliation.
32
+
33
+ - **Class A — status-mismatch**: free-text field contains done-words (`done|completed|shipped|deployed|finished`) while `status` is partial (`completed-partial|blocked|in-progress|failed`).
34
+ - **Class B — negation-vs-evidence**: free-text contains a negated claim (`no outages`, `0 regressions`) while `hotfixes[]`, `incidents[]`, or `regressions[]` is non-empty.
35
+
36
+ **Why**: mechanical gates (test counts, lint, tsc) catch implementation errors. Narrative-quality claims in free-text fields (`notes`, `result`, `summary`, `description`) get rubber-stamped. This scan compares narrative against adjacent structured fields.
37
+
38
+ **Mode**: surface-and-prompt, non-blocking. A hard-fail at session-end has no recovery path.
39
+
40
+ **Enforcement**: `scripts/flow-completion-truth-gate.js` → `scanForClaimContradictions()`. Invoked by `flow-session-end.js` and `flow-health.js`.
41
+
42
+ ---
43
+
44
+ ### Merge-Plan Artifact Gate
45
+
46
+ **Rule**: `/wogi-finalize` requires `.workflow/scratch/merge-plan.md` for any merge with more than `config.finalization.mergePlan.threshold` commits (default 5) OR any cross-repo merge. The plan must map every commit in `git log <base>..<branch>` to one of: `port | adapt | skip-style | superseded | skip-with-reason`.
47
+
48
+ **Mechanical invariant**: count of SHA-prefixed lines in the plan MUST equal `git log <base>..<branch> | wc -l`. Mismatch blocks the merge.
49
+
50
+ **Structural-change sensor**: when ≥ `config.finalization.mergePlan.restructureThreshold` (default 20%) of changed files match a restructure pattern (folder-per-component, split-into-submodule, barrel-introduction, rename-new-home), a structural warning prefixes the plan and biases affected commits toward `adapt`.
51
+
52
+ **Enforcement**: `scripts/flow-structure-sensor.js`, `.claude/commands/wogi-finalize.md` Step 2.5.
53
+
54
+ ---
55
+
56
+ ### Story Creation Quality Gates
57
+
58
+ **Rule**: `/wogi-story` enforces five P0 specification-quality gates at creation time. Gates answer *"is the story clear, complete, checkable?"* — NOT *"is the implementation correct?"* (the latter remains `/wogi-start`'s job).
59
+
60
+ 1. **Long Input** — ≥40 lines OR ≥5 discrete items → route to `/wogi-extract-review` for zero-loss capture.
61
+ 2. **Item Reconciliation** — ≥3 discrete items → enumerated "Item Manifest" section; every item must appear in at least one criterion or sub-task. Unmapped items surface as a warning.
62
+ 3. **Consumer Impact Analysis** — refactoring keywords (`refactor`, `rename`, `migrate`, `split`, `extract`, ...) trigger `git grep` for consumers. ≥5 breaking consumers → phased migration recommendation.
63
+ 4. **Scope-Confidence Audit** — assumption patterns (`new <X>`, `existing <Y>`, `the <Z> service`) are verified against the codebase; findings go into a "Pending Clarifications" block.
64
+ 5. **Intent Bootstrap Coordination** — schedules IGR artifact bootstrap via `intentBootstrapScheduledAt` flag so `/wogi-story` and `/wogi-start` don't both prompt.
65
+
66
+ **Guard-rails**: all gates fail-open (grep failure, classifier unavailable → warning, story still created). Gates may be bypassed via `--skip-gates` for testing.
67
+
68
+ **Config**: `storyFlow.consumerImpactAnalysis.*`, `storyFlow.scopeConfidenceAudit.*`, `storyFlow.itemReconciliation.*` in `.workflow/config.json`.
69
+
70
+ ---
71
+
72
+ ### Workspace Autonomous-Mode Action-After-Completion Contract
73
+
74
+ **Applies to**: workspace worker mode (`WOGI_WORKSPACE_ROOT` set + `WOGI_REPO_NAME !== 'manager'`).
75
+
76
+ **Rule**: A worker's end-of-turn must be a deterministic action. Exactly one of these states must hold:
77
+
78
+ 1. **ACTION** — started the next pre-approved channel dispatch (invoked `/wogi-start <nextId>`), OR
79
+ 2. **ESCALATION** — channel-dispatched a `## QUESTION:` to the manager (after local resolution attempts failed), OR
80
+ 3. **IDLE** — zero pending channel dispatches AND zero in-progress tasks.
81
+
82
+ **Hedging language is mechanically forbidden**: *"awaiting your signal"*, *"let me know if"*, *"should I continue"*, *"standing by"*, *"ready when you are"*. These invent an imaginary decision point — the manager already pre-approved the dispatch by queuing it. Visibility is NOT a substitute for action; workers narrate AND act in the same turn.
83
+
84
+ **Enforcement**: `TaskCompleted` hook emits auto-pickup when queued dispatches exist. `Stop` hook blocks end-of-turn when a worker has queued dispatches but no in-progress task. `worker-rules.md` template carries the 3-state contract.
85
+
86
+ **Config**: `workspace.autoPickupChannelDispatches` (default `true`).
87
+
88
+ ---
89
+
90
+ ### Workspace Worker Cannot Prompt User Directly
91
+
92
+ **Applies to**: workspace worker mode.
93
+
94
+ **Rule**: The `AskUserQuestion` tool is mechanically blocked in worker mode. Questions to the user MUST be channel-dispatched to the manager via `## QUESTION: ...`.
95
+
96
+ **Why block instead of auto-redirect**: the worker must consciously choose between (a) channel-dispatching the real question to the manager for user input, or (b) making a reasonable autonomous decision and noting it in the task reply. Silent redirection removes that choice.
97
+
98
+ **Enforcement**: `scripts/hooks/core/worker-boundary-gate.js` → `checkWorkerBoundary()`. PreToolUse hook blocks `AskUserQuestion`; block message includes the exact `curl ... --data-binary "## QUESTION: ..."` command. Config: `workspace.blockAskUserQuestionInWorker` (default `true`).
99
+
100
+ ---
101
+
102
+ ### Workspace Worker Text-Question Classifier
103
+
104
+ **Applies to**: workspace worker mode.
105
+
106
+ **Rule**: If a worker ends a turn with a text-based question to the user (no tool call — just hedging: *"let me know"*, *"should I"*, *"which option"*, *"thoughts?"*, trailing `?`), the Stop hook runs a Haiku classifier on the final assistant message. If it detects an open question with confidence ≥ `minConfidence` → stop is blocked with channel-dispatch instructions.
107
+
108
+ **Why AI instead of regex**: hedging vocabulary is infinite. Regex misses novel phrasings.
109
+
110
+ **Fail-open throughout**: missing `ANTHROPIC_API_KEY`, missing transcript path, malformed transcript, or model error → skip. Silent-stall false negatives are recoverable; false-positive blocks every turn are not.
111
+
112
+ **Enforcement**: `scripts/flow-worker-question-classifier.js`. Config: `workspace.aiWorkerQuestionClassifier.{enabled,minConfidence,model}`.
113
+
114
+ ---
115
+
116
+ ### Workspace Worker Silent-Halt Detection
117
+
118
+ **Applies to**: workspace manager mode.
119
+
120
+ **Rule**: Every dispatch to a worker MUST be tracked. Any pending dispatch past its `expectedDeadline` with no matching `task-complete` or `worker-stopped` message = silent death, surfaced on the manager's next turn.
121
+
122
+ **Three terminal states**:
123
+ 1. **Completed** — `task-complete` message arrived.
124
+ 2. **Graceful-stop** — `worker-stopped` message arrived (worker's Stop hook fired, but didn't complete).
125
+ 3. **Silent-halt** — no message, deadline passed. Worker probably dead.
126
+
127
+ **Deadline**: default `expectedDurationMs` = 30 min. Callers override per-dispatch for long tasks.
128
+
129
+ **Architecture — file-based, hook-driven, no background processes**:
130
+ - `lib/workspace-dispatch-tracking.js` — record / reconcile / overdue helpers
131
+ - `.workspace/state/dispatched-tasks.json` — ring buffer of last 100 active records
132
+ - Manager's `dispatchToChannel()` calls `recordDispatch()` after successful POST
133
+ - Manager's `UserPromptSubmit` hook sweeps the message bus and surfaces overdue records as `additionalContext`
134
+
135
+ ---
136
+
137
+ ### Code Quality Patterns (generic)
138
+
139
+ These apply to any codebase being built with WogiFlow's help.
140
+
141
+ **1. Single Source of Truth for Constants** — avoid duplicating model/configuration objects across files. Import from one canonical location. Prevents drift and makes updates simpler.
142
+
143
+ **2. Named Constants for Magic Numbers** — define thresholds and limits as named constants; don't inline literals.
144
+
145
+ ```js
146
+ const COVERAGE_THRESHOLDS = { default: 0.7, comprehensive: 0.85, concise: 0.5 };
147
+ ```
148
+
149
+ Self-documenting; easier to maintain.
@@ -710,6 +710,50 @@ async function dispatchToChannel(workspaceRoot, repoName, taskId, opts = {}) {
710
710
  return { ok: false, message: `Invalid task ID format: "${taskId}" — expected wf-XXXXXXXX` };
711
711
  }
712
712
 
713
+ // Research-before-propose gate (manager mode): require evidence that the
714
+ // manager has read at least N state/spec files before dispatching work.
715
+ //
716
+ // Architectural note (ARCH-005): the dispatch gate is wired here in the
717
+ // routing layer rather than via pre-tool-orchestrator because channel
718
+ // dispatch is a lib-level operation that can be invoked outside the
719
+ // Claude Code PreToolUse hook path (e.g., by a worker's internal CLI
720
+ // or by a script). Wiring here ensures the gate fires on every dispatch,
721
+ // regardless of invocation surface. The spec-write gate is wired in the
722
+ // orchestrator because Edit/Write is hook-surface-only.
723
+ //
724
+ // Error handling (CL-005): separate catches so gate-not-installed (the
725
+ // intended fail-open path) is distinct from config-load failures (which
726
+ // indicate broken installs worth surfacing in DEBUG) and from gate
727
+ // runtime errors (which suggest a bug in the gate itself).
728
+ let dispatchGateModule = null;
729
+ try {
730
+ dispatchGateModule = require('../scripts/hooks/core/research-evidence-gate');
731
+ } catch (_err) {
732
+ // Gate module not installed — fail-open silently; this is expected on
733
+ // older installs that predate the research-evidence gate.
734
+ }
735
+ if (dispatchGateModule) {
736
+ let cfg = null;
737
+ try {
738
+ const { getConfig } = require('../scripts/flow-utils');
739
+ cfg = getConfig();
740
+ } catch (err) {
741
+ if (process.env.DEBUG) {
742
+ console.error(`[dispatchToChannel] Config load failed (gate still enforced with defaults): ${err.message}`);
743
+ }
744
+ }
745
+ try {
746
+ const dispatchGate = dispatchGateModule.checkDispatchEvidenceGate(cfg);
747
+ if (dispatchGate.blocked) {
748
+ return { ok: false, message: dispatchGate.message };
749
+ }
750
+ } catch (err) {
751
+ if (process.env.DEBUG) {
752
+ console.error(`[dispatchToChannel] Dispatch gate runtime error (fail-open): ${err.message}`);
753
+ }
754
+ }
755
+ }
756
+
713
757
  const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
714
758
  const config = safeReadJson(configPath);
715
759
  if (!config || typeof config !== 'object') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.25.1",
3
+ "version": "2.26.1",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -986,7 +986,8 @@ const CONFIG_DEFAULTS = {
986
986
  configChange: { enabled: false },
987
987
  setup: { enabled: true, autoOnboard: false, maintenanceTasks: ['healthCheck', 'cleanupLocks'] },
988
988
  sessionCleanup: { enabled: true },
989
- phaseGate: { enabled: true }
989
+ phaseGate: { enabled: true },
990
+ researchEvidenceGate: { enabled: true, minEvidence: 2 }
990
991
  }
991
992
  },
992
993
  claudeCode: { installPath: '.claude/settings.local.json' }
@@ -1066,6 +1067,14 @@ const CONFIG_DEFAULTS = {
1066
1067
  // --- Decisions ---
1067
1068
  decisions: { amendmentTracking: { enabled: false } },
1068
1069
 
1070
+ // --- Session End ---
1071
+ // autoPushInWorker: when /wogi-session-end runs inside a workspace worker
1072
+ // (WOGI_WORKSPACE_ROOT set, WOGI_REPO_NAME !== 'manager'), the worker must
1073
+ // not prompt the user directly. true (default) → auto-push silently.
1074
+ // false → skip push entirely; user pushes manually. Non-worker sessions
1075
+ // are unaffected and still see the interactive prompt.
1076
+ sessionEnd: { autoPushInWorker: true },
1077
+
1069
1078
  // --- Community ---
1070
1079
  community: {
1071
1080
  enabled: false,
@@ -41,6 +41,17 @@ const { getReadyData, saveReadyData } = require('./flow-utils');
41
41
  // v2.6.1: Use centralized state cleanup module
42
42
  const { cleanupStaleState } = require('./flow-state-cleanup');
43
43
 
44
+ // Workspace worker detection — used by offerPush() to branch between
45
+ // interactive prompt (single-repo) and silent auto-push (worker mode).
46
+ // Loaded eagerly; if module is missing on a broken install, fail loud
47
+ // rather than masking the issue via a lazy require deep in offerPush().
48
+ let isWorker;
49
+ try {
50
+ isWorker = require('../lib/workspace-worker-ready').isWorker;
51
+ } catch (_err) {
52
+ isWorker = () => false;
53
+ }
54
+
44
55
  // v1.8.0 automatic memory management
45
56
  let memoryDb = null;
46
57
  try {
@@ -448,22 +459,63 @@ function analyzeCrossSessionPatterns() {
448
459
  }
449
460
 
450
461
  /**
451
- * Offer to push to remote
462
+ * Offer to push to remote.
463
+ *
464
+ * In workspace worker mode (WOGI_WORKSPACE_ROOT + WOGI_REPO_NAME !== 'manager'),
465
+ * workers must not prompt the user directly — that violates the Workspace Worker
466
+ * Cannot Prompt User Directly contract (decisions.md v2.20.1+). Instead:
467
+ * - config.sessionEnd.autoPushInWorker === true (default) → push silently
468
+ * - config.sessionEnd.autoPushInWorker === false → skip push silently
469
+ * In single-repo mode (the common case), the interactive prompt is unchanged.
452
470
  */
453
471
  async function offerPush() {
454
472
  if (!isGitRepo()) return;
455
473
 
456
474
  try {
457
475
  execSync('git remote get-url origin', { stdio: 'pipe' });
476
+ } catch (_err) {
477
+ return;
478
+ }
458
479
 
459
- const confirm = await prompt('Push to remote? (y/N) ');
480
+ // Worker-mode detection + secondary validation (SEC-001). isWorker() checks
481
+ // WOGI_WORKSPACE_ROOT + WOGI_REPO_NAME env vars. For defense in depth,
482
+ // confirm that `.workspace/` exists inside WOGI_WORKSPACE_ROOT before
483
+ // treating env-var signals as authoritative — this narrows the window in
484
+ // which a misconfigured single-repo session could be misidentified as a
485
+ // worker and auto-push without confirmation.
486
+ if (isWorker() && isValidWorkspaceRoot()) {
487
+ const autoPush = getConfigValue('sessionEnd.autoPushInWorker', true);
488
+ if (!autoPush) {
489
+ console.log(color('dim', '⊘ Push skipped (worker mode — autoPushInWorker: false)'));
490
+ return;
491
+ }
492
+ try {
493
+ execSync('git push', { stdio: 'inherit' });
494
+ success('Auto-pushed (worker mode)');
495
+ } catch (err) {
496
+ warn(`Auto-push failed: ${err.message}`);
497
+ }
498
+ return;
499
+ }
460
500
 
501
+ try {
502
+ const confirm = await prompt('Push to remote? (y/N) ');
461
503
  if (confirm.toLowerCase() === 'y') {
462
504
  execSync('git push', { stdio: 'inherit' });
463
505
  success('Pushed to remote');
464
506
  }
465
507
  } catch (_err) {
466
- // No remote configured, skip
508
+ // Prompt or push failed, skip
509
+ }
510
+ }
511
+
512
+ function isValidWorkspaceRoot() {
513
+ const root = process.env.WOGI_WORKSPACE_ROOT;
514
+ if (!root || !path.isAbsolute(root)) return false;
515
+ try {
516
+ return fs.existsSync(path.join(root, '.workspace'));
517
+ } catch (_err) {
518
+ return false;
467
519
  }
468
520
  }
469
521
 
@@ -164,6 +164,28 @@ function transitionPhase(from, to, taskId) {
164
164
  return false;
165
165
  }
166
166
 
167
+ // Research-evidence gate: transitions into proposal phases (spec_review,
168
+ // coding) require minimum evidence fingerprint. Fail-open if gate module
169
+ // is absent. Prints the block message to stderr so flow-phase.js CLI
170
+ // surfaces it to the AI invoking the transition.
171
+ if (to === 'spec_review' || to === 'coding') {
172
+ try {
173
+ const { checkPhaseTransitionEvidence } = require('./research-evidence-gate');
174
+ let cfg = null;
175
+ try {
176
+ const { getConfig } = require('../../flow-utils');
177
+ cfg = getConfig();
178
+ } catch (_err) { /* fail-open on config error */ }
179
+ const result = checkPhaseTransitionEvidence(from, to, cfg);
180
+ if (result.blocked) {
181
+ console.error(result.message);
182
+ return false;
183
+ }
184
+ } catch (_err) {
185
+ // Gate not installed — fail-open
186
+ }
187
+ }
188
+
167
189
  return writePhaseState({
168
190
  phase: to,
169
191
  taskId: taskId || current.taskId,
@@ -167,6 +167,21 @@ function handlePostCompact() {
167
167
  }
168
168
  }
169
169
 
170
+ // Clear research-evidence fingerprint after compaction — the AI has fresh
171
+ // context, so claims of "already read X" in the previous context no longer
172
+ // apply. The gate must force re-reading in the new context.
173
+ try {
174
+ const { clearResearchEvidence } = require('./research-evidence-gate');
175
+ clearResearchEvidence();
176
+ if (process.env.DEBUG) {
177
+ console.error('[post-compact] Research-evidence cleared');
178
+ }
179
+ } catch (err) {
180
+ if (process.env.DEBUG) {
181
+ console.error(`[post-compact] Research-evidence clear failed: ${err.message}`);
182
+ }
183
+ }
184
+
170
185
  // 3. Re-set routing-pending flag
171
186
  // After compaction, the AI has fresh context and may try to act without routing.
172
187
  // Setting routing-pending ensures the next tool use goes through routing checks.
@@ -80,6 +80,9 @@ function runPreToolGates(ctx, deps) {
80
80
  // Phase-read recording (side effect)
81
81
  if (toolName === 'Read' && filePath) {
82
82
  try { deps.recordPhaseRead(filePath); } catch (_err) { /* fail-open */ }
83
+ if (deps.recordEvidenceRead) {
84
+ try { deps.recordEvidenceRead(filePath); } catch (_err) { /* fail-open */ }
85
+ }
83
86
  }
84
87
 
85
88
  // Phase gate
@@ -114,6 +117,24 @@ function runPreToolGates(ctx, deps) {
114
117
  }
115
118
  }
116
119
 
120
+ // Research-evidence gate (spec-write): blocks Edit/Write to proposal paths
121
+ // when the AI has not read enough evidence files this task turn.
122
+ if ((toolName === 'Edit' || toolName === 'Write') && deps.checkSpecWriteGate) {
123
+ try {
124
+ const specResult = deps.checkSpecWriteGate(filePath, config);
125
+ if (specResult.blocked) {
126
+ return {
127
+ allowed: false,
128
+ blocked: true,
129
+ reason: 'Research-evidence gate: insufficient research before proposal',
130
+ message: specResult.message,
131
+ };
132
+ }
133
+ } catch (_err) {
134
+ if (process.env.DEBUG) console.error(`[Hook] Research-evidence gate error (fail-open): ${_err.message}`);
135
+ }
136
+ }
137
+
117
138
  // Scope gate (Edit/Write only)
118
139
  if (toolName === 'Edit' || toolName === 'Write') {
119
140
  coreResult = deps.checkScopeGate({ filePath, operation: toolName.toLowerCase() }, config);
@@ -133,6 +154,9 @@ function runPreToolGates(ctx, deps) {
133
154
  if (typeof skillName === 'string' && /^wogi-(bulk|start)$/i.test(skillName)) {
134
155
  deps.markSkillPending(skillName.toLowerCase(), { args: toolInput.args });
135
156
  try { deps.clearPhaseReads(); } catch (_err) { /* fail-open */ }
157
+ if (deps.clearResearchEvidence) {
158
+ try { deps.clearResearchEvidence(); } catch (_err) { /* fail-open */ }
159
+ }
136
160
  if (process.env.DEBUG) {
137
161
  console.error(`[Hook] Marked skill ${skillName} as pending (via Skill tool)`);
138
162
  }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Research-Evidence Gate (Core Module)
5
+ *
6
+ * Enforces the "Research Before Propose" methodology rule by tracking which
7
+ * state/spec/epic files the AI has Read in the current task turn, and blocking
8
+ * proposal actions (spec writes, channel-dispatch to workers) until a minimum
9
+ * evidence threshold has been reached.
10
+ *
11
+ * Does NOT block AskUserQuestion, plain Read/Glob/Grep/WebSearch, conversational
12
+ * text, or non-proposal edits. Asking the user is a valid escape hatch.
13
+ *
14
+ * State file: .workflow/state/research-evidence.json
15
+ * Fail-open: If state file is missing/corrupt or config disabled, allow the tool call.
16
+ *
17
+ * Three entry points:
18
+ * recordEvidenceRead(filePath) — called when Read targets an evidence file
19
+ * checkSpecWriteGate(filePath, config) — called before Edit/Write to proposal paths
20
+ * checkDispatchEvidenceGate(config) — called before manager channel-dispatch
21
+ * clearResearchEvidence() — called on new task start / session end / post-compact
22
+ */
23
+
24
+ const path = require('node:path');
25
+ const fs = require('node:fs');
26
+ const { PATHS, safeJsonParse } = require('../../flow-utils');
27
+
28
+ const EVIDENCE_FILE = path.join(PATHS.state, 'research-evidence.json');
29
+
30
+ // Relative-to-project path prefixes that count as evidence when Read.
31
+ // Any file whose project-relative path starts with one of these prefixes
32
+ // increments the evidence counter.
33
+ const EVIDENCE_PREFIXES = [
34
+ '.workflow/state/',
35
+ '.workflow/changes/',
36
+ '.workflow/specs/',
37
+ '.workflow/epics/'
38
+ ];
39
+
40
+ // Path prefixes that trigger the spec-write gate when targeted by Edit/Write.
41
+ // Writing to these paths = "proposing a spec" = must have evidence first.
42
+ const PROPOSAL_PREFIXES = [
43
+ '.workflow/changes/',
44
+ '.workflow/specs/',
45
+ '.workflow/epics/'
46
+ ];
47
+
48
+ // Default threshold: minimum number of distinct evidence-file reads required
49
+ // before a proposal action is allowed. Can be overridden by config.
50
+ const DEFAULT_MIN_EVIDENCE = 2;
51
+
52
+ function toProjectRelative(filePath) {
53
+ try {
54
+ // Canonicalize both sides via realpath to prevent symlink escape
55
+ // (SEC-003): a symlink in PATHS.root or the input path could make a
56
+ // file outside the project appear inside after a plain path.relative.
57
+ let rootCanon = PATHS.root;
58
+ let targetCanon = path.resolve(filePath);
59
+ try { rootCanon = fs.realpathSync(PATHS.root); } catch (_err) { /* root may not exist mid-test */ }
60
+ try { targetCanon = fs.realpathSync(targetCanon); } catch (_err) { /* target may not exist yet */ }
61
+ const rel = path.relative(rootCanon, targetCanon);
62
+ return rel.split(path.sep).join('/');
63
+ } catch (_err) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function matchesPrefix(relPath, prefixes) {
69
+ if (!relPath || relPath.startsWith('..')) return false;
70
+ return prefixes.some(p => relPath.startsWith(p));
71
+ }
72
+
73
+ /**
74
+ * Record that an evidence file was read. Called from PreToolUse on Read.
75
+ * De-duplicates: reading the same file twice still counts as 1.
76
+ *
77
+ * Write strategy (CL-001): atomic temp-file + rename. A concurrent tool call
78
+ * that loses the read-modify-write race will at worst lose one evidence entry,
79
+ * which causes a false-block that the user resolves by reading one more file.
80
+ * The atomic rename prevents partial writes on crash.
81
+ */
82
+ function recordEvidenceRead(filePath) {
83
+ if (!filePath || typeof filePath !== 'string') return;
84
+
85
+ const rel = toProjectRelative(filePath);
86
+ if (!matchesPrefix(rel, EVIDENCE_PREFIXES)) return;
87
+
88
+ try {
89
+ const existing = safeJsonParse(EVIDENCE_FILE, {});
90
+ if (!existing.reads || typeof existing.reads !== 'object') existing.reads = {};
91
+ if (!existing.reads[rel]) {
92
+ existing.reads[rel] = { at: new Date().toISOString() };
93
+ const tmp = `${EVIDENCE_FILE}.${process.pid}.tmp`;
94
+ fs.writeFileSync(tmp, JSON.stringify(existing, null, 2));
95
+ fs.renameSync(tmp, EVIDENCE_FILE);
96
+ if (process.env.DEBUG) {
97
+ console.error(`[ResearchEvidenceGate] Recorded evidence read: ${rel}`);
98
+ }
99
+ }
100
+ } catch (err) {
101
+ if (process.env.DEBUG) {
102
+ console.error(`[ResearchEvidenceGate] Failed to record read: ${err.message}`);
103
+ }
104
+ }
105
+ }
106
+
107
+ function getEvidenceCount() {
108
+ try {
109
+ const data = safeJsonParse(EVIDENCE_FILE, {});
110
+ if (!data.reads || typeof data.reads !== 'object') return 0;
111
+ return Object.keys(data.reads).length;
112
+ } catch (_err) {
113
+ return 0;
114
+ }
115
+ }
116
+
117
+ function isGateEnabled(config) {
118
+ const gateCfg = config?.hooks?.rules?.researchEvidenceGate;
119
+ if (gateCfg === undefined || gateCfg === null) return true;
120
+ if (gateCfg === false) return false;
121
+ if (typeof gateCfg === 'object' && gateCfg.enabled === false) return false;
122
+ return true;
123
+ }
124
+
125
+ function getMinEvidence(config) {
126
+ const v = config?.hooks?.rules?.researchEvidenceGate?.minEvidence;
127
+ if (typeof v === 'number' && v >= 0 && Number.isFinite(v)) return v;
128
+ return DEFAULT_MIN_EVIDENCE;
129
+ }
130
+
131
+ /**
132
+ * Block Edit/Write to a proposal path when evidence fingerprint is below threshold.
133
+ * Called from pre-tool-orchestrator before Edit/Write runs.
134
+ *
135
+ * @param {string} filePath - Path being written/edited
136
+ * @param {Object} config
137
+ * @returns {{ blocked: boolean, message?: string }}
138
+ */
139
+ function checkSpecWriteGate(filePath, config) {
140
+ try {
141
+ if (!isGateEnabled(config)) return { blocked: false };
142
+ if (!filePath || typeof filePath !== 'string') return { blocked: false };
143
+
144
+ const rel = toProjectRelative(filePath);
145
+ if (!matchesPrefix(rel, PROPOSAL_PREFIXES)) return { blocked: false };
146
+
147
+ const minEvidence = getMinEvidence(config);
148
+ const count = getEvidenceCount();
149
+ if (count >= minEvidence) return { blocked: false };
150
+
151
+ return {
152
+ blocked: true,
153
+ message:
154
+ `Research-before-propose: this writes a spec/change/epic (${rel}), but you have only ` +
155
+ `read ${count} evidence file(s) this task turn. Minimum required: ${minEvidence}.\n\n` +
156
+ `Before proposing, read relevant files from:\n` +
157
+ ` .workflow/state/decisions.md, feedback-patterns.md, app-map.md, function-map.md, api-map.md\n` +
158
+ ` the task spec (.workflow/changes/<taskId>.md or .workflow/specs/<id>.md)\n` +
159
+ ` .workflow/epics/ if this task belongs to an epic\n\n` +
160
+ `If you genuinely need clarification before proposing, use AskUserQuestion — that is allowed.\n` +
161
+ `The rule is "don't propose before researching," not "never ask."`
162
+ };
163
+ } catch (err) {
164
+ if (process.env.DEBUG) {
165
+ console.error(`[ResearchEvidenceGate] Spec-write gate error (fail-open): ${err.message}`);
166
+ }
167
+ return { blocked: false };
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Block channel-dispatch to a worker when the manager has no evidence of
173
+ * having researched the target's state. Called from lib/workspace-routing.js
174
+ * before dispatchToChannel posts.
175
+ *
176
+ * @param {Object} config
177
+ * @returns {{ blocked: boolean, message?: string }}
178
+ */
179
+ function checkDispatchEvidenceGate(config) {
180
+ try {
181
+ if (!isGateEnabled(config)) return { blocked: false };
182
+
183
+ // Manager-mode only: workers don't dispatch. Single-repo sessions
184
+ // (no WOGI_WORKSPACE_ROOT) are n/a — there are no workers to dispatch
185
+ // to, so the evidence requirement does not apply (CL-003).
186
+ const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
187
+ const repo = process.env.WOGI_REPO_NAME;
188
+ const isManager = !!workspaceRoot && (!repo || repo === 'manager');
189
+ if (!isManager) return { blocked: false };
190
+
191
+ const minEvidence = getMinEvidence(config);
192
+ const count = getEvidenceCount();
193
+ if (count >= minEvidence) return { blocked: false };
194
+
195
+ return {
196
+ blocked: true,
197
+ message:
198
+ `Research-before-dispatch: dispatching to a worker proposes work, but the manager has only ` +
199
+ `read ${count} evidence file(s) this turn. Minimum required: ${minEvidence}.\n\n` +
200
+ `Before dispatching, read relevant state from the target member repo:\n` +
201
+ ` <member-repo>/.workflow/state/decisions.md, app-map.md, feedback-patterns.md\n` +
202
+ ` <member-repo>/.workflow/changes/ for existing task specs\n\n` +
203
+ `Silent workers that receive poorly-specified work cost the most to recover. ` +
204
+ `The Wogi Hub manager incident that prompted this rule dispatched Employee-class ` +
205
+ `clarifying-question work without reading the existing class system.`
206
+ };
207
+ } catch (err) {
208
+ if (process.env.DEBUG) {
209
+ console.error(`[ResearchEvidenceGate] Dispatch gate error (fail-open): ${err.message}`);
210
+ }
211
+ return { blocked: false };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Block phase transitions into proposal phases (spec_review, coding) when
217
+ * the AI has not read enough evidence files this task turn. Called from
218
+ * phase-gate.js transitionPhase() when the target is a proposal phase.
219
+ *
220
+ * @param {string} from
221
+ * @param {string} to
222
+ * @param {Object} config
223
+ * @returns {{ blocked: boolean, message?: string }}
224
+ */
225
+ function checkPhaseTransitionEvidence(from, to, config) {
226
+ try {
227
+ if (!isGateEnabled(config)) return { blocked: false };
228
+ if (to !== 'spec_review' && to !== 'coding') return { blocked: false };
229
+
230
+ const minEvidence = getMinEvidence(config);
231
+ const count = getEvidenceCount();
232
+ if (count >= minEvidence) return { blocked: false };
233
+
234
+ return {
235
+ blocked: true,
236
+ message:
237
+ `Research-before-propose: transitioning to "${to}" requires ${minEvidence} evidence ` +
238
+ `file read(s) this task turn; you have ${count}.\n\n` +
239
+ `Before transitioning to a proposal phase, read relevant files from:\n` +
240
+ ` .workflow/state/decisions.md, feedback-patterns.md, app-map.md, function-map.md\n` +
241
+ ` the task spec (.workflow/changes/<taskId>.md or .workflow/specs/<id>.md)\n` +
242
+ ` .workflow/epics/ if this task belongs to an epic\n\n` +
243
+ `AskUserQuestion is not blocked — ask if clarification is genuinely needed.`
244
+ };
245
+ } catch (err) {
246
+ if (process.env.DEBUG) {
247
+ console.error(`[ResearchEvidenceGate] Phase-transition gate error (fail-open): ${err.message}`);
248
+ }
249
+ return { blocked: false };
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Clear evidence state. Called at:
255
+ * - New task start (pre-tool-use.js Skill hook for wogi-start)
256
+ * - Session end
257
+ * - Post-compact (forces re-read in new context)
258
+ */
259
+ function clearResearchEvidence() {
260
+ try {
261
+ fs.writeFileSync(EVIDENCE_FILE, JSON.stringify({ reads: {} }, null, 2));
262
+ } catch (err) {
263
+ if (process.env.DEBUG) {
264
+ console.error(`[ResearchEvidenceGate] Failed to clear evidence: ${err.message}`);
265
+ }
266
+ }
267
+ }
268
+
269
+ module.exports = {
270
+ recordEvidenceRead,
271
+ checkSpecWriteGate,
272
+ checkDispatchEvidenceGate,
273
+ checkPhaseTransitionEvidence,
274
+ clearResearchEvidence,
275
+ getEvidenceCount,
276
+ EVIDENCE_FILE,
277
+ EVIDENCE_PREFIXES,
278
+ PROPOSAL_PREFIXES,
279
+ DEFAULT_MIN_EVIDENCE
280
+ };
@@ -110,6 +110,13 @@ function handleSessionEnd(input) {
110
110
  // Non-critical — phase-read gate may not be installed
111
111
  }
112
112
 
113
+ try {
114
+ const { clearResearchEvidence } = require('./research-evidence-gate');
115
+ clearResearchEvidence();
116
+ } catch (_err) {
117
+ // Non-critical — research-evidence gate may not be installed
118
+ }
119
+
113
120
  // State folder hygiene — clean stale/orphan files (fire-and-forget)
114
121
  try {
115
122
  const hygiene = cleanStaleFiles();
@@ -50,6 +50,12 @@ const { getConfig, PATHS } = require('../../flow-utils');
50
50
  const { safeJsonParse } = require('../../flow-io');
51
51
 
52
52
  const PENDING_MARKER_FILE = 'task-just-completed';
53
+ const LAST_TRIGGERED_FILE = 'task-boundary-last-triggered';
54
+ // Window during which a recentlyCompleted[0] entry is considered "fresh
55
+ // enough" to retro-mark Phase 1 from the Stop hook. Large enough to cover
56
+ // a slow quality-gate run; small enough that a session opened hours later
57
+ // doesn't trigger a bogus restart.
58
+ const FRESHNESS_WINDOW_MS = 5 * 60 * 1000;
53
59
 
54
60
  /**
55
61
  * Locate the pending-marker file path inside .workflow/state/.
@@ -59,6 +65,26 @@ function getPendingMarkerPath() {
59
65
  return path.join(PATHS.state, PENDING_MARKER_FILE);
60
66
  }
61
67
 
68
+ function getLastTriggeredPath() {
69
+ return path.join(PATHS.state, LAST_TRIGGERED_FILE);
70
+ }
71
+
72
+ function readLastTriggered() {
73
+ try {
74
+ return safeJsonParse(getLastTriggeredPath(), null);
75
+ } catch (_err) {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function writeLastTriggered(taskId) {
81
+ try {
82
+ const p = getLastTriggeredPath();
83
+ fs.mkdirSync(path.dirname(p), { recursive: true });
84
+ fs.writeFileSync(p, JSON.stringify({ taskId, at: new Date().toISOString() }));
85
+ } catch (_err) { /* best effort — anti-replay is defense-in-depth */ }
86
+ }
87
+
62
88
  /**
63
89
  * Phase 1 — mark that a task just completed and a restart is desired at the
64
90
  * next Stop-hook boundary. Safe to call even when the feature is disabled;
@@ -196,6 +222,13 @@ function consumeAndTriggerRestart() {
196
222
  return { triggered: false, reason: `sigterm-failed: ${err.message}` };
197
223
  }
198
224
 
225
+ // Record anti-replay sentinel so the Stop-hook fallback in the NEW session
226
+ // (post-restart) doesn't retro-mark the same recentlyCompleted[0] and
227
+ // trigger a second restart.
228
+ if (markerPayload?.taskId) {
229
+ writeLastTriggered(markerPayload.taskId);
230
+ }
231
+
199
232
  return {
200
233
  triggered: true,
201
234
  flagPath: pre.flagPath,
@@ -203,6 +236,72 @@ function consumeAndTriggerRestart() {
203
236
  };
204
237
  }
205
238
 
239
+ /**
240
+ * Phase 1 fallback — called from the Stop hook BEFORE
241
+ * consumeAndTriggerRestart. Detects a freshly-completed task in
242
+ * recentlyCompleted and writes the pending marker if neither of the primary
243
+ * Phase 1 paths fired.
244
+ *
245
+ * Why this exists: the primary Phase 1 writers are (a) flow-done.js:604 when
246
+ * `flow done <taskId>` runs, and (b) task-completed.js:522 driven by Claude
247
+ * Code's TaskCompleted hook. Path (b) does not fire for /wogi-start workflow
248
+ * completions (TaskCompleted fires for Task-tool sub-agents only — the reason
249
+ * for the two-phase redesign above). Path (a) only fires if the agent runs
250
+ * `flow done`. Older phase docs quietly encouraged "move task to
251
+ * recentlyCompleted in ready.json" as a substitute for `flow done`, which
252
+ * silently disables the restart. This fallback catches that case: if a fresh
253
+ * completion is visible in ready.json but no marker exists, we write one so
254
+ * Phase 2 can do its job.
255
+ *
256
+ * Anti-replay: recentlyCompleted[0] survives the SIGTERM + wrapper restart
257
+ * cycle, so without a guard the Stop hook in the NEW session would see the
258
+ * same fresh completion and trigger a second restart. The
259
+ * task-boundary-last-triggered sentinel prevents that — it records the last
260
+ * taskId we triggered on, and we skip if the current fresh completion
261
+ * matches.
262
+ *
263
+ * @returns {{ marked: boolean, taskId?: string, reason?: string }}
264
+ */
265
+ function ensurePhase1MarkedIfRecentlyCompleted() {
266
+ try {
267
+ if (hasPendingMarker()) {
268
+ return { marked: false, reason: 'marker-already-present' };
269
+ }
270
+
271
+ const readyPath = path.join(PATHS.state, 'ready.json');
272
+ const ready = safeJsonParse(readyPath, null);
273
+ const recent = ready && Array.isArray(ready.recentlyCompleted)
274
+ ? ready.recentlyCompleted[0]
275
+ : null;
276
+ if (!recent || typeof recent !== 'object' || !recent.id || !recent.completedAt) {
277
+ return { marked: false, reason: 'no-fresh-completion' };
278
+ }
279
+
280
+ const completedTs = new Date(recent.completedAt).getTime();
281
+ if (!Number.isFinite(completedTs)) {
282
+ return { marked: false, reason: 'unparseable-completedAt' };
283
+ }
284
+ const ageMs = Date.now() - completedTs;
285
+ if (ageMs < 0 || ageMs > FRESHNESS_WINDOW_MS) {
286
+ return { marked: false, reason: 'stale-completion' };
287
+ }
288
+
289
+ const lastTriggered = readLastTriggered();
290
+ if (lastTriggered?.taskId === recent.id) {
291
+ return { marked: false, reason: 'already-triggered-for-this-task' };
292
+ }
293
+
294
+ const result = markRestartPending({
295
+ taskId: recent.id,
296
+ taskTitle: recent.title,
297
+ source: 'stop-hook-fallback'
298
+ });
299
+ return { marked: result.marked, taskId: recent.id, reason: result.reason };
300
+ } catch (err) {
301
+ return { marked: false, reason: `fallback-error: ${err.message}` };
302
+ }
303
+ }
304
+
206
305
  /**
207
306
  * Convenience: whether a pending marker currently exists. Diagnostic only.
208
307
  * @returns {boolean}
@@ -219,6 +318,10 @@ module.exports = {
219
318
  // Phase 1 — called from task-completion code paths
220
319
  markRestartPending,
221
320
 
321
+ // Phase 1 fallback — called from the Stop hook entry BEFORE Phase 2,
322
+ // catches the case where flow-done didn't run and TaskCompleted didn't fire
323
+ ensurePhase1MarkedIfRecentlyCompleted,
324
+
222
325
  // Phase 2 — called from the Stop hook entry
223
326
  consumeAndTriggerRestart,
224
327
 
@@ -35,6 +35,20 @@ try {
35
35
  clearPhaseReads = prg.clearPhaseReads;
36
36
  } catch (_err) { if (process.env.DEBUG) console.error(`[Hook] Phase-read gate not loaded: ${_err.message}`); }
37
37
 
38
+ let recordEvidenceRead = () => {}, checkSpecWriteGate = () => ({ blocked: false }), clearResearchEvidence = () => {};
39
+ try {
40
+ const reg = require('../../core/research-evidence-gate');
41
+ recordEvidenceRead = reg.recordEvidenceRead;
42
+ checkSpecWriteGate = reg.checkSpecWriteGate;
43
+ clearResearchEvidence = reg.clearResearchEvidence;
44
+ } catch (err) {
45
+ // CL-004: load failure for a gate file that SHOULD be present is a
46
+ // deployment issue worth surfacing even without DEBUG set. Silently
47
+ // shimming masks broken installs. Preserve fail-open (shims above)
48
+ // so the hook pipeline still works, but log to stderr so operators see it.
49
+ console.error(`[Hook] WARNING: Research-evidence gate failed to load — gate is disabled. ${err.message}`);
50
+ }
51
+
38
52
  const _noop = () => ({ allowed: true, blocked: false });
39
53
  let checkDeployGate = _noop, checkWriteBlock = _noop;
40
54
  try { const dg = require('../../core/deploy-gate'); checkDeployGate = dg.checkDeployGate; checkWriteBlock = dg.checkWriteBlock; } catch (_err) { if (process.env.DEBUG) console.error(`[Hook] Deploy gate not loaded: ${_err.message}`); }
@@ -84,6 +98,7 @@ runHook('PreToolUse', async ({ input, parsedInput }) => {
84
98
  checkRoutingGate, clearRoutingPending, hasActiveTask,
85
99
  checkPhaseGate, checkCommitLogGate,
86
100
  recordPhaseRead, checkPhaseReadGate, clearPhaseReads,
101
+ recordEvidenceRead, checkSpecWriteGate, clearResearchEvidence,
87
102
  checkDeployGate, checkWriteBlock,
88
103
  checkStrikeGate, checkBugfixScope, checkScopeMutation,
89
104
  checkGitSafety, checkManagerBoundary, checkWorkerBoundary,
@@ -155,7 +155,33 @@ runHook('Stop', async ({ parsedInput }) => {
155
155
  // No-op unless task-just-completed marker exists AND feature is enabled
156
156
  // AND wogi-claude wrapper env is present.
157
157
  try {
158
- const { consumeAndTriggerRestart, hasPendingMarker } = require('../../core/task-boundary-reset');
158
+ const {
159
+ consumeAndTriggerRestart,
160
+ hasPendingMarker,
161
+ ensurePhase1MarkedIfRecentlyCompleted
162
+ } = require('../../core/task-boundary-reset');
163
+
164
+ // Phase 1 fallback: if the task completed via a path that didn't write the
165
+ // marker (e.g., agent edited ready.json directly instead of running
166
+ // `flow done`, or TaskCompleted hook didn't fire), retro-mark here so
167
+ // Phase 2 below can consume it. Anti-replay sentinel prevents double-firing
168
+ // across the SIGTERM + wrapper restart cycle.
169
+ try {
170
+ const fallback = ensurePhase1MarkedIfRecentlyCompleted();
171
+ if (fallback.marked && process.env.DEBUG) {
172
+ console.error(`[Stop] Phase 1 fallback marked ${fallback.taskId}`);
173
+ } else if (!fallback.marked && fallback.reason !== 'marker-already-present' &&
174
+ fallback.reason !== 'no-fresh-completion' &&
175
+ fallback.reason !== 'stale-completion' &&
176
+ fallback.reason !== 'already-triggered-for-this-task' &&
177
+ process.env.DEBUG) {
178
+ console.error(`[Stop] Phase 1 fallback skipped: ${fallback.reason}`);
179
+ }
180
+ } catch (err) {
181
+ if (process.env.DEBUG) {
182
+ console.error(`[Stop] Phase 1 fallback error (fail-open): ${err.message}`);
183
+ }
184
+ }
159
185
 
160
186
  // If we're about to restart, record the session in history FIRST so the
161
187
  // new session can find the prior session's resume token. Use parsedInput
@@ -1,7 +0,0 @@
1
- {
2
- "taskId": null,
3
- "uniqueFiles": [],
4
- "thresholdReached": false,
5
- "scopeInventory": null,
6
- "warnedAt": null
7
- }
@@ -1,3 +0,0 @@
1
- {
2
- "deploys": []
3
- }
@@ -1,4 +0,0 @@
1
- {
2
- "routes": [],
3
- "lastUpdated": null
4
- }
@@ -1,3 +0,0 @@
1
- {
2
- "tasks": {}
3
- }