wogiflow 2.33.0 → 2.34.2

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 (58) hide show
  1. package/.workflow/templates/partials/methodology-rules.hbs +3 -1
  2. package/lib/scheduled-mode.js +12 -15
  3. package/lib/skill-export-claude-plugin.js +41 -1
  4. package/lib/skill-portability.js +21 -3
  5. package/lib/workspace-channel-server.js +116 -3
  6. package/lib/workspace-channel-tracking.js +102 -1
  7. package/lib/workspace-dispatch-tracking.js +28 -0
  8. package/lib/workspace-messages.js +32 -4
  9. package/lib/workspace-subtask-state.js +215 -0
  10. package/lib/workspace.js +81 -0
  11. package/package.json +2 -2
  12. package/scripts/flow +17 -0
  13. package/scripts/flow-constants.js +3 -1
  14. package/scripts/flow-io.js +17 -0
  15. package/scripts/flow-paths.js +81 -0
  16. package/scripts/flow-schedule.js +23 -6
  17. package/scripts/flow-scheduled-runner.js +53 -8
  18. package/scripts/flow-standards-checker.js +37 -0
  19. package/scripts/flow-utils.js +2 -0
  20. package/scripts/hooks/adapters/claude-code.js +6 -2
  21. package/scripts/hooks/core/git-safety-gate.js +34 -15
  22. package/scripts/hooks/core/long-input-enforcement.js +49 -39
  23. package/scripts/hooks/core/overdue-dispatches.js +28 -6
  24. package/scripts/hooks/core/phase-gate.js +34 -5
  25. package/scripts/hooks/core/phase-read-gate.js +62 -10
  26. package/scripts/hooks/core/session-start-worker.js +52 -0
  27. package/scripts/hooks/core/stop-orchestrator.js +17 -2
  28. package/scripts/hooks/core/validation.js +8 -0
  29. package/scripts/hooks/core/worker-continuation-gate.js +487 -0
  30. package/scripts/hooks/core/workspace-stop-gates.js +21 -0
  31. package/scripts/hooks/core/workspace-stop-notify.js +174 -59
  32. package/scripts/hooks/entry/claude-code/post-tool-use.js +26 -0
  33. package/.claude/rules/README.md +0 -36
  34. package/.claude/rules/_internal/README.md +0 -64
  35. package/.claude/rules/_internal/document-structure.md +0 -77
  36. package/.claude/rules/_internal/dual-repo-management.md +0 -174
  37. package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
  38. package/.claude/rules/_internal/github-releases.md +0 -71
  39. package/.claude/rules/_internal/model-management.md +0 -35
  40. package/.claude/rules/_internal/self-maintenance.md +0 -87
  41. package/.claude/rules/_internal/worker-tool-first-turn.md +0 -82
  42. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +0 -11
  43. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +0 -11
  44. package/.claude/rules/alternative-hook-args-exec-form.md +0 -6
  45. package/.claude/rules/alternative-permission-ruleset-per-phase.md +0 -11
  46. package/.claude/rules/alternative-short-name.md +0 -12
  47. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +0 -11
  48. package/.claude/rules/architecture/component-reuse.md +0 -38
  49. package/.claude/rules/architecture/hook-three-layer.md +0 -68
  50. package/.claude/rules/code-style/naming-conventions.md +0 -107
  51. package/.claude/rules/dual-repo-architecture-2026-02-28.md +0 -18
  52. package/.claude/rules/github-release-workflow-2026-01-30.md +0 -16
  53. package/.claude/rules/operations/git-workflows.md +0 -92
  54. package/.claude/rules/operations/scratch-directory.md +0 -54
  55. package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
  56. package/.workflow/specs/architecture.md.template +0 -24
  57. package/.workflow/specs/stack.md.template +0 -33
  58. package/.workflow/specs/testing.md.template +0 -36
@@ -23,10 +23,31 @@
23
23
 
24
24
  const path = require('node:path');
25
25
  const fs = require('node:fs');
26
- const { PATHS, safeJsonParse } = require('../../flow-utils');
26
+ const { PATHS, safeJsonParse, getCanonicalStateDir, isLinkedWorktree } = require('../../flow-utils');
27
27
 
28
+ // Exported for tests (main-tree path). The gate resolves these paths CANONICALLY
29
+ // at call time (see workflowPhasePath / phaseReadsPath) so it cannot be evaded
30
+ // from a git worktree, where the gitignored phase file is absent (wf-e5e57361 /
31
+ // RC2). In the main working tree the canonical path equals this constant.
28
32
  const PHASE_READS_FILE = path.join(PATHS.state, 'phase-reads.json');
29
- const WORKFLOW_PHASE_FILE = path.join(PATHS.state, 'workflow-phase.json');
33
+
34
+ // Lazy canonical resolvers — `stateDir` override is injectable for tests.
35
+ function workflowPhasePath(stateDir) { return path.join(stateDir || getCanonicalStateDir(), 'workflow-phase.json'); }
36
+ function phaseReadsPath(stateDir) { return path.join(stateDir || getCanonicalStateDir(), 'phase-reads.json'); }
37
+
38
+ /**
39
+ * True if the canonical ready.json shows a task in-progress. Used to fail the
40
+ * gate CLOSED (rather than open) when phase state is unresolvable inside a
41
+ * worktree of an in-progress task — the gate-evasion shape from RC2.
42
+ */
43
+ function hasCanonicalInProgressTask(stateDir) {
44
+ try {
45
+ const ready = safeJsonParse(path.join(stateDir || getCanonicalStateDir(), 'ready.json'), { inProgress: [] });
46
+ return Array.isArray(ready.inProgress) && ready.inProgress.length > 0;
47
+ } catch (_err) {
48
+ return false;
49
+ }
50
+ }
30
51
 
31
52
  // Maps workflow phases to required instruction files
32
53
  const PHASE_FILE_REGISTRY = {
@@ -80,7 +101,8 @@ function recordPhaseRead(filePath) {
80
101
  // fail-open semantics mean a lost write just means the gate asks for a
81
102
  // re-read — safe degradation, not a correctness bug.
82
103
  try {
83
- const existing = safeJsonParse(PHASE_READS_FILE, {});
104
+ const readsFile = phaseReadsPath();
105
+ const existing = safeJsonParse(readsFile, {});
84
106
  if (!existing.reads) existing.reads = {};
85
107
 
86
108
  existing.reads[matchedPhase] = {
@@ -88,7 +110,8 @@ function recordPhaseRead(filePath) {
88
110
  at: new Date().toISOString()
89
111
  };
90
112
 
91
- fs.writeFileSync(PHASE_READS_FILE, JSON.stringify(existing, null, 2));
113
+ fs.mkdirSync(path.dirname(readsFile), { recursive: true });
114
+ fs.writeFileSync(readsFile, JSON.stringify(existing, null, 2));
92
115
 
93
116
  if (process.env.DEBUG) {
94
117
  console.error(`[PhaseReadGate] Recorded read of ${PHASE_FILE_REGISTRY[matchedPhase]} for phase ${matchedPhase}`);
@@ -113,8 +136,10 @@ function recordPhaseRead(filePath) {
113
136
  *
114
137
  * @param {string} toolName
115
138
  * @param {Object} [config] - Optional config object
139
+ * @param {Object} [deps] - Injectable seams for tests:
140
+ * { stateDir, isLinkedWorktree, hasInProgressTask }
116
141
  */
117
- function checkPhaseReadGate(toolName, config) {
142
+ function checkPhaseReadGate(toolName, config, deps = {}) {
118
143
  try {
119
144
  // Respect phaseReadGate config with fallback to phaseGate (backwards compat).
120
145
  // If phaseReadGate.enabled is explicitly false, skip. If it's undefined,
@@ -129,10 +154,32 @@ function checkPhaseReadGate(toolName, config) {
129
154
  return { blocked: false };
130
155
  }
131
156
 
132
- // Read current phase
133
- const phaseData = safeJsonParse(WORKFLOW_PHASE_FILE, null);
157
+ // RC2 (wf-e5e57361): resolve phase from the CANONICAL state dir, not cwd —
158
+ // a git worktree lacks the gitignored phase file, so a cwd-relative read
159
+ // would fail-open to an unrestricted "idle" phase ("ungated context").
160
+ const stateDir = deps.stateDir || getCanonicalStateDir();
161
+ const inWorktree = (deps.isLinkedWorktree || isLinkedWorktree);
162
+ const hasInProgress = (deps.hasInProgressTask || hasCanonicalInProgressTask);
163
+
164
+ // Read current phase (canonically)
165
+ const phaseData = safeJsonParse(workflowPhasePath(stateDir), null);
134
166
  if (!phaseData || !phaseData.phase) {
135
- return { blocked: false }; // No phase data = fail-open
167
+ // RC2 fail-CLOSED: a missing phase file inside a linked worktree while a
168
+ // task is in-progress (per canonical ready.json) is the gate-evasion
169
+ // shape — block mutation tools instead of failing open.
170
+ if ((toolName === 'Edit' || toolName === 'Write' || toolName === 'Bash') &&
171
+ inWorktree(stateDir) && hasInProgress(stateDir)) {
172
+ return {
173
+ blocked: true,
174
+ message: 'Phase gate (RC2): a task is in progress but the workflow phase ' +
175
+ 'could not be resolved, and you appear to be operating from a git worktree. ' +
176
+ 'Gates are NOT evadable by working from a worktree — phase is resolved from ' +
177
+ 'the canonical (main-repo) state. Return to the main working tree and satisfy ' +
178
+ 'the gate legitimately, or channel-escalate to the manager. Do NOT create ' +
179
+ 'worktrees or write gate-satisfying markers to bypass this.'
180
+ };
181
+ }
182
+ return { blocked: false }; // No phase data = fail-open (normal main-tree)
136
183
  }
137
184
 
138
185
  const currentPhase = phaseData.phase;
@@ -149,7 +196,7 @@ function checkPhaseReadGate(toolName, config) {
149
196
  }
150
197
 
151
198
  // Check if that file has been read
152
- const readData = safeJsonParse(PHASE_READS_FILE, {});
199
+ const readData = safeJsonParse(phaseReadsPath(stateDir), {});
153
200
  const reads = readData.reads || {};
154
201
 
155
202
  if (reads[currentPhase]) {
@@ -182,7 +229,9 @@ function checkPhaseReadGate(toolName, config) {
182
229
  */
183
230
  function clearPhaseReads() {
184
231
  try {
185
- fs.writeFileSync(PHASE_READS_FILE, JSON.stringify({ reads: {} }, null, 2));
232
+ const readsFile = phaseReadsPath();
233
+ fs.mkdirSync(path.dirname(readsFile), { recursive: true });
234
+ fs.writeFileSync(readsFile, JSON.stringify({ reads: {} }, null, 2));
186
235
  } catch (_err) {
187
236
  if (process.env.DEBUG) {
188
237
  console.error(`[PhaseReadGate] Failed to clear phase reads: ${_err.message}`);
@@ -194,6 +243,9 @@ module.exports = {
194
243
  recordPhaseRead,
195
244
  checkPhaseReadGate,
196
245
  clearPhaseReads,
246
+ hasCanonicalInProgressTask,
247
+ workflowPhasePath,
248
+ phaseReadsPath,
197
249
  PHASE_FILE_REGISTRY,
198
250
  PHASE_READS_FILE
199
251
  };
@@ -40,6 +40,58 @@ function handleWorkerSessionStart() {
40
40
  const { isWorker, shouldAnnounceReady, announceWorkerReady } = require(WORKER_READY_LIB);
41
41
  if (!isWorker()) return { branch: 'skip', reason: 'not-worker' };
42
42
 
43
+ // S5 (wf-ee87a24e): RESUME-IN-PROGRESS. If this restarted session has a task
44
+ // still in `inProgress` with sub-tasks remaining (durable S1 ledger), resume
45
+ // THAT task — do NOT fall through to "announce idle" (which would orphan it)
46
+ // or pick a different next task. The durable ledger means completed sub-tasks
47
+ // are NOT redone. Also post a worker-ready ack so the manager actively
48
+ // re-triggers if the resume wake-up was missed.
49
+ try {
50
+ const { PATHS, safeJsonParse } = require('../../flow-utils');
51
+ const ready = safeJsonParse(path.join(PATHS.state, 'ready.json'), { inProgress: [] });
52
+ const inProgress = (ready.inProgress || [])[0] || null;
53
+ if (inProgress && inProgress.id) {
54
+ let remaining = null, total = null;
55
+ try {
56
+ const subtaskState = require(path.join(__dirname, '..', '..', '..', 'lib', 'workspace-subtask-state.js'));
57
+ const summary = subtaskState.summary(inProgress.id);
58
+ remaining = summary.remaining; total = summary.total;
59
+ } catch (_err) { /* ledger optional */ }
60
+ // Only treat as resumable if there is remaining decomposed work, OR no
61
+ // ledger exists at all (single-step task interrupted mid-flight).
62
+ if (remaining === null || remaining > 0) {
63
+ // Best-effort ack so the manager knows the worker is back on this task.
64
+ // Bypass shouldAnnounceReady's empty-queue gating (it returns
65
+ // 'in-progress-not-empty' here by design) — for a resume we WANT the
66
+ // manager pinged. announceWorkerReady dedups via hasPendingAnnounce.
67
+ try {
68
+ const wr = require(WORKER_READY_LIB);
69
+ const wsRoot = process.env.WOGI_WORKSPACE_ROOT;
70
+ const repoName = process.env.WOGI_REPO_NAME;
71
+ if (wsRoot && repoName && repoName !== 'manager') {
72
+ wr.announceWorkerReady(wsRoot, repoName);
73
+ }
74
+ } catch (_err) { /* ack is best-effort */ }
75
+ const ctx = [
76
+ `⚡ WORKSPACE SESSION START — RESUMING IN-PROGRESS TASK`,
77
+ '',
78
+ `This worker restarted with task ${inProgress.id} still in progress${total != null ? ` (${remaining} of ${total} sub-task(s) remaining)` : ''}.`,
79
+ `Durable sub-task state is on disk — completed sub-tasks are recorded and must NOT be redone.`,
80
+ '',
81
+ 'AUTONOMOUS MODE CONTRACT (workspace worker):',
82
+ ' • Resume the SAME task — do not pick a different one, do not go idle.',
83
+ ' • Read .workflow/state/subtask-state.json to see which sub-tasks remain.',
84
+ ' • Grind to completion; only stop when done (flow done) or genuinely blocked.',
85
+ '',
86
+ `ACT NOW: Invoke Skill(skill="wogi-start", args="${inProgress.id}")`
87
+ ].join('\n');
88
+ return { branch: 'resume-in-progress', context: ctx, taskId: inProgress.id, remaining, total };
89
+ }
90
+ }
91
+ } catch (err) {
92
+ if (process.env.DEBUG) console.error(`[session-start-worker] resume-in-progress check failed (fail-open): ${err.message}`);
93
+ }
94
+
43
95
  // Check for queued work first — if any, tell the model to pick it up
44
96
  // instead of announcing idle readiness.
45
97
  let pickup;
@@ -87,8 +87,12 @@ async function orchestrateStop({ parsedInput }) {
87
87
  };
88
88
  }
89
89
 
90
+ // S3 (wf-d3ae1717): the worker-stopped emission used to fire HERE,
91
+ // unconditionally, before any gate decided to continue — so the manager saw
92
+ // "stopped mid-work" on every turn boundary. It now fires only at a genuine
93
+ // stop (end of this function) with a precise terminal type, and a
94
+ // worker-progress heartbeat fires from the continuation gate instead.
90
95
  const workspaceNotify = require('./workspace-stop-notify');
91
- await workspaceNotify.notifyWorkerStopped();
92
96
 
93
97
  const restartCoordinator = require('./task-boundary-restart-coordinator');
94
98
  const restartResult = await restartCoordinator.handleTaskBoundaryRestart({ parsedInput });
@@ -120,7 +124,18 @@ async function orchestrateStop({ parsedInput }) {
120
124
  const wsResult = await workspaceGates.checkWorkspaceStopGates({ parsedInput });
121
125
  if (wsResult?.shouldReturn) return wsResult.result;
122
126
 
123
- return await checkLoopExit();
127
+ // Genuine stop path: no gate forced continuation. Emit a precise terminal
128
+ // worker signal ONLY when we're actually allowing the turn to end (canExit).
129
+ // continueToNext / blocked-continue are not terminal stops.
130
+ const loopResult = await checkLoopExit();
131
+ try {
132
+ if (loopResult?.canExit === true) {
133
+ await workspaceNotify.notifyWorkerTerminal();
134
+ }
135
+ } catch (err) {
136
+ if (process.env.DEBUG) console.error(`[Stop] terminal notify error (fail-open): ${err.message}`);
137
+ }
138
+ return loopResult;
124
139
  }
125
140
 
126
141
  module.exports = { orchestrateStop };
@@ -222,6 +222,14 @@ async function runValidation(options = {}) {
222
222
 
223
223
  return {
224
224
  passed: allPassed,
225
+ // F6 (R-379): signal `blocked` so the adapter's `decision: 'block'` path
226
+ // actually fires when validation fails. Without this, the `continueOnBlock`
227
+ // wiring in transformPostToolUse is inert (decision is always undefined).
228
+ // With it, lint/typecheck failure after Edit/Write feeds back to Claude
229
+ // and (per the continueOnBlock setting) the turn continues so Claude can
230
+ // fix the error in-loop — which is what CLAUDE.md's "validate after every
231
+ // file edit" rule needs.
232
+ blocked: !allPassed,
225
233
  skipped: false,
226
234
  results,
227
235
  summary: generateValidationSummary(results, filePath)