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.
- package/.workflow/templates/partials/methodology-rules.hbs +3 -1
- package/lib/scheduled-mode.js +12 -15
- package/lib/skill-export-claude-plugin.js +41 -1
- package/lib/skill-portability.js +21 -3
- package/lib/workspace-channel-server.js +116 -3
- package/lib/workspace-channel-tracking.js +102 -1
- package/lib/workspace-dispatch-tracking.js +28 -0
- package/lib/workspace-messages.js +32 -4
- package/lib/workspace-subtask-state.js +215 -0
- package/lib/workspace.js +81 -0
- package/package.json +2 -2
- package/scripts/flow +17 -0
- package/scripts/flow-constants.js +3 -1
- package/scripts/flow-io.js +17 -0
- package/scripts/flow-paths.js +81 -0
- package/scripts/flow-schedule.js +23 -6
- package/scripts/flow-scheduled-runner.js +53 -8
- package/scripts/flow-standards-checker.js +37 -0
- package/scripts/flow-utils.js +2 -0
- package/scripts/hooks/adapters/claude-code.js +6 -2
- package/scripts/hooks/core/git-safety-gate.js +34 -15
- package/scripts/hooks/core/long-input-enforcement.js +49 -39
- package/scripts/hooks/core/overdue-dispatches.js +28 -6
- package/scripts/hooks/core/phase-gate.js +34 -5
- package/scripts/hooks/core/phase-read-gate.js +62 -10
- package/scripts/hooks/core/session-start-worker.js +52 -0
- package/scripts/hooks/core/stop-orchestrator.js +17 -2
- package/scripts/hooks/core/validation.js +8 -0
- package/scripts/hooks/core/worker-continuation-gate.js +487 -0
- package/scripts/hooks/core/workspace-stop-gates.js +21 -0
- package/scripts/hooks/core/workspace-stop-notify.js +174 -59
- package/scripts/hooks/entry/claude-code/post-tool-use.js +26 -0
- package/.claude/rules/README.md +0 -36
- package/.claude/rules/_internal/README.md +0 -64
- package/.claude/rules/_internal/document-structure.md +0 -77
- package/.claude/rules/_internal/dual-repo-management.md +0 -174
- package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
- package/.claude/rules/_internal/github-releases.md +0 -71
- package/.claude/rules/_internal/model-management.md +0 -35
- package/.claude/rules/_internal/self-maintenance.md +0 -87
- package/.claude/rules/_internal/worker-tool-first-turn.md +0 -82
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +0 -11
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +0 -11
- package/.claude/rules/alternative-hook-args-exec-form.md +0 -6
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +0 -11
- package/.claude/rules/alternative-short-name.md +0 -12
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +0 -11
- package/.claude/rules/architecture/component-reuse.md +0 -38
- package/.claude/rules/architecture/hook-three-layer.md +0 -68
- package/.claude/rules/code-style/naming-conventions.md +0 -107
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +0 -18
- package/.claude/rules/github-release-workflow-2026-01-30.md +0 -16
- package/.claude/rules/operations/git-workflows.md +0 -92
- package/.claude/rules/operations/scratch-directory.md +0 -54
- package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
- package/.workflow/specs/architecture.md.template +0 -24
- package/.workflow/specs/stack.md.template +0 -33
- 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
|
-
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
133
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|