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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Durable Sub-task State (epic-workspace-sustained-exec / S1, wf-e72350bf)
|
|
5
|
+
*
|
|
6
|
+
* Sub-tasks of a decomposed task are normally tracked ONLY in Claude Code's
|
|
7
|
+
* in-context TodoWrite list, which is ephemeral: a Stop hook running in a fresh
|
|
8
|
+
* `node` process can't read it, and a session restart loses it entirely. That's
|
|
9
|
+
* the root cause behind workers re-executing completed sub-tasks after a restart
|
|
10
|
+
* and behind the continuation gate (S2) having no reliable "work remaining"
|
|
11
|
+
* signal.
|
|
12
|
+
*
|
|
13
|
+
* This module mirrors the decomposition to a durable, atomically-written ledger
|
|
14
|
+
* at `.workflow/state/subtask-state.json` so that:
|
|
15
|
+
* - a fresh-process Stop hook can read "N of M sub-tasks remain" (S2),
|
|
16
|
+
* - a restarted session can resume without redoing completed sub-tasks (S5).
|
|
17
|
+
*
|
|
18
|
+
* The ledger holds the CURRENT in-progress task's decomposition. There is one
|
|
19
|
+
* active task per worker at a time, so a single-task shape is sufficient and
|
|
20
|
+
* avoids stale cross-task contamination — read* helpers take an optional taskId
|
|
21
|
+
* and return empty when it doesn't match the ledger's task.
|
|
22
|
+
*
|
|
23
|
+
* Atomic write: tmp + fsync(file) + rename + fsync(dir), mirroring
|
|
24
|
+
* task-boundary-reset.js:writeCleanCompletionMarker so the ledger survives the
|
|
25
|
+
* SIGTERM/relaunch boundary and concurrent readers never see torn JSON.
|
|
26
|
+
*
|
|
27
|
+
* Fail-open throughout: any error degrades to "no durable state" (callers fall
|
|
28
|
+
* back to their prior behavior). Never throws.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
const { PATHS } = require('../scripts/flow-utils');
|
|
35
|
+
const { safeJsonParse } = require('../scripts/flow-io');
|
|
36
|
+
|
|
37
|
+
const LEDGER_FILE = 'subtask-state.json';
|
|
38
|
+
const LEDGER_VERSION = 1;
|
|
39
|
+
// Statuses that count as "still needs work" for remaining().
|
|
40
|
+
const OPEN_STATUSES = new Set(['pending', 'in_progress']);
|
|
41
|
+
const TERMINAL_STATUSES = new Set(['completed', 'blocked']);
|
|
42
|
+
|
|
43
|
+
function getLedgerPath() {
|
|
44
|
+
return path.join(PATHS.state, LEDGER_FILE);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a free-form sub-task entry (TodoWrite item or plain object/string)
|
|
49
|
+
* into the ledger shape { id, title, status }.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeSubtask(entry, index) {
|
|
52
|
+
if (entry == null) return null;
|
|
53
|
+
if (typeof entry === 'string') {
|
|
54
|
+
return { id: String(index + 1).padStart(2, '0'), title: entry.slice(0, 500), status: 'pending' };
|
|
55
|
+
}
|
|
56
|
+
if (typeof entry !== 'object') return null;
|
|
57
|
+
const rawStatus = typeof entry.status === 'string' ? entry.status.toLowerCase() : 'pending';
|
|
58
|
+
const status = OPEN_STATUSES.has(rawStatus) || TERMINAL_STATUSES.has(rawStatus) ? rawStatus : 'pending';
|
|
59
|
+
const title = entry.title || entry.content || entry.text || entry.description || entry.activeForm || `Sub-task ${index + 1}`;
|
|
60
|
+
const id = entry.id != null ? String(entry.id) : String(index + 1).padStart(2, '0');
|
|
61
|
+
return { id, title: String(title).slice(0, 500), status };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Map a Claude Code TodoWrite toolInput ({ todos: [{ content, status }] })
|
|
66
|
+
* into normalized sub-tasks. Returns [] for anything unparseable.
|
|
67
|
+
*/
|
|
68
|
+
function subtasksFromTodos(toolInput) {
|
|
69
|
+
try {
|
|
70
|
+
const todos = toolInput && Array.isArray(toolInput.todos) ? toolInput.todos : null;
|
|
71
|
+
if (!todos) return [];
|
|
72
|
+
return todos.map(normalizeSubtask).filter(Boolean);
|
|
73
|
+
} catch (_err) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read the full ledger object, or null if absent/unreadable.
|
|
80
|
+
* @returns {{version:number, taskId:string, updatedAt:string, subtasks:Array}|null}
|
|
81
|
+
*/
|
|
82
|
+
function readLedger() {
|
|
83
|
+
try {
|
|
84
|
+
const data = safeJsonParse(getLedgerPath(), null);
|
|
85
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.subtasks)) return null;
|
|
86
|
+
return data;
|
|
87
|
+
} catch (_err) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read the sub-tasks for a given task. When taskId is provided and doesn't match
|
|
94
|
+
* the ledger's task, returns [] (the ledger belongs to a different task).
|
|
95
|
+
* @param {string} [taskId]
|
|
96
|
+
* @returns {Array<{id:string,title:string,status:string}>}
|
|
97
|
+
*/
|
|
98
|
+
function read(taskId) {
|
|
99
|
+
const led = readLedger();
|
|
100
|
+
if (!led) return [];
|
|
101
|
+
if (taskId && led.taskId && led.taskId !== taskId) return [];
|
|
102
|
+
return led.subtasks;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Write/replace the ledger for taskId atomically.
|
|
107
|
+
* @param {string} taskId
|
|
108
|
+
* @param {Array} subtasks raw or normalized entries
|
|
109
|
+
* @returns {{written:boolean, reason?:string, path?:string}}
|
|
110
|
+
*/
|
|
111
|
+
function write(taskId, subtasks) {
|
|
112
|
+
if (!taskId) return { written: false, reason: 'no-task-id' };
|
|
113
|
+
try {
|
|
114
|
+
const normalized = (Array.isArray(subtasks) ? subtasks : [])
|
|
115
|
+
.map(normalizeSubtask)
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
const payload = {
|
|
118
|
+
version: LEDGER_VERSION,
|
|
119
|
+
taskId,
|
|
120
|
+
updatedAt: new Date().toISOString(),
|
|
121
|
+
subtasks: normalized
|
|
122
|
+
};
|
|
123
|
+
const p = getLedgerPath();
|
|
124
|
+
const dir = path.dirname(p);
|
|
125
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
+
const tmp = `${p}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
127
|
+
const fd = fs.openSync(tmp, 'w');
|
|
128
|
+
try {
|
|
129
|
+
fs.writeSync(fd, JSON.stringify(payload, null, 2));
|
|
130
|
+
fs.fsyncSync(fd);
|
|
131
|
+
} finally {
|
|
132
|
+
fs.closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
fs.renameSync(tmp, p);
|
|
135
|
+
try {
|
|
136
|
+
const dfd = fs.openSync(dir, 'r');
|
|
137
|
+
try { fs.fsyncSync(dfd); } finally { fs.closeSync(dfd); }
|
|
138
|
+
} catch (_err) { /* directory fsync best-effort (not supported on all FS) */ }
|
|
139
|
+
return { written: true, path: p };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return { written: false, reason: `write-failed: ${err.message}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Count sub-tasks still needing work (pending + in_progress) for taskId.
|
|
147
|
+
* blocked and completed do NOT count. Returns 0 when no matching ledger exists
|
|
148
|
+
* (no durable state ⇒ "nothing known to remain"; callers decide what that means).
|
|
149
|
+
* @param {string} [taskId]
|
|
150
|
+
* @returns {number}
|
|
151
|
+
*/
|
|
152
|
+
function remaining(taskId) {
|
|
153
|
+
const subs = read(taskId);
|
|
154
|
+
return subs.filter(s => OPEN_STATUSES.has(s.status)).length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Summary counts for heartbeats / status (S3, S4).
|
|
159
|
+
* @param {string} [taskId]
|
|
160
|
+
* @returns {{total:number, remaining:number, completed:number, blocked:number}}
|
|
161
|
+
*/
|
|
162
|
+
function summary(taskId) {
|
|
163
|
+
const subs = read(taskId);
|
|
164
|
+
return {
|
|
165
|
+
total: subs.length,
|
|
166
|
+
remaining: subs.filter(s => OPEN_STATUSES.has(s.status)).length,
|
|
167
|
+
completed: subs.filter(s => s.status === 'completed').length,
|
|
168
|
+
blocked: subs.filter(s => s.status === 'blocked').length
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Mark one sub-task's status, preserving the rest. No-op if the ledger task
|
|
174
|
+
* doesn't match. Returns the updated count.
|
|
175
|
+
* @param {string} taskId
|
|
176
|
+
* @param {string} subId
|
|
177
|
+
* @param {string} [status='completed']
|
|
178
|
+
*/
|
|
179
|
+
function markStatus(taskId, subId, status = 'completed') {
|
|
180
|
+
const led = readLedger();
|
|
181
|
+
if (!led || (taskId && led.taskId !== taskId)) return { written: false, reason: 'no-matching-ledger' };
|
|
182
|
+
const next = led.subtasks.map(s => (s.id === String(subId) ? { ...s, status } : s));
|
|
183
|
+
return write(led.taskId, next);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Clear the ledger (e.g. on task completion). Best-effort.
|
|
188
|
+
*/
|
|
189
|
+
function clear() {
|
|
190
|
+
try {
|
|
191
|
+
fs.unlinkSync(getLedgerPath());
|
|
192
|
+
return { cleared: true };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (err && err.code !== 'ENOENT' && process.env.DEBUG) {
|
|
195
|
+
console.error(`[workspace-subtask-state] clear: ${err.code} ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
return { cleared: false };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
getLedgerPath,
|
|
203
|
+
normalizeSubtask,
|
|
204
|
+
subtasksFromTodos,
|
|
205
|
+
readLedger,
|
|
206
|
+
read,
|
|
207
|
+
write,
|
|
208
|
+
remaining,
|
|
209
|
+
summary,
|
|
210
|
+
markStatus,
|
|
211
|
+
clear,
|
|
212
|
+
LEDGER_FILE,
|
|
213
|
+
OPEN_STATUSES,
|
|
214
|
+
TERMINAL_STATUSES
|
|
215
|
+
};
|
package/lib/workspace.js
CHANGED
|
@@ -1209,6 +1209,18 @@ You are a workspace worker. There is NO human watching your terminal. You MUST o
|
|
|
1209
1209
|
|
|
1210
1210
|
The \`TaskCompleted\` hook will inject an auto-pickup directive when channel dispatches are queued (v2.20.0+). The \`Stop\` hook will BLOCK end-of-turn if you try to stop while dispatches are queued and no task is in progress. These enforcements exist because the silent-stall pattern was incident-worthy (2026-04-16).
|
|
1211
1211
|
|
|
1212
|
+
### Sustained Execution — Grind a Dispatched Task to Completion (v2.34.0+)
|
|
1213
|
+
|
|
1214
|
+
**A dispatched task runs to COMPLETION across turns. Do NOT stop to "report progress" after each sub-task.** When you decompose a task into sub-tasks (TodoWrite), work through ALL of them in the same session — the decomposition is mirrored to durable state (\`subtask-state.json\`) so it survives turn boundaries.
|
|
1215
|
+
|
|
1216
|
+
"The work" in *"reply to the manager after completing the work"* means the **entire dispatched task** (all its sub-tasks), not one increment. Reply with \`## Results\` ONLY when:
|
|
1217
|
+
- the task is **complete** (you ran \`flow done <id>\` and it left inProgress), or
|
|
1218
|
+
- you are **escalating** a genuine blocker (\`## QUESTION:\` / \`## BLOCKED:\`).
|
|
1219
|
+
|
|
1220
|
+
The \`Stop\` hook's **in-progress continuation gate** will force you to keep going (\`{continue:true}\`) while a task is in-progress in the coding/validating phase with sub-tasks remaining. If you stop early to "give visibility," it will re-prompt you to continue. Make real progress every turn (an edit, a test, a completed sub-task) — idle turns with no file changes are detected across a few continuations and stop with an escalation.
|
|
1221
|
+
|
|
1222
|
+
**Escalate instead of proceeding** when the next step is destructive, irreversible, touches production, needs external credentials, or genuinely needs a human decision. Otherwise: keep grinding until done.
|
|
1223
|
+
|
|
1212
1224
|
### Tool-First Turn Contract (v2.27.0+)
|
|
1213
1225
|
|
|
1214
1226
|
**Every worker turn after a UserPromptSubmit MUST contain at least one tool call. In strict mode (default), the FIRST content block must be a tool call, not text.**
|
|
@@ -1768,6 +1780,61 @@ function startWorkerSession(cwd) {
|
|
|
1768
1780
|
* Handle workspace subcommands
|
|
1769
1781
|
* @param {string[]} args
|
|
1770
1782
|
*/
|
|
1783
|
+
/**
|
|
1784
|
+
* S5 (wf-ee87a24e): manager-triggered worker restart. Resolves the worker's
|
|
1785
|
+
* channel port from the workspace config and POSTs /restart. The worker's
|
|
1786
|
+
* channel server writes the wrapper restart flag and SIGTERMs claude, so the
|
|
1787
|
+
* wogi-claude wrapper relaunches it with a fresh require cache (reloading any
|
|
1788
|
+
* upgraded wogiflow code). Resume of the in-progress task is handled by the
|
|
1789
|
+
* worker's SessionStart hook (resume-in-progress branch).
|
|
1790
|
+
*/
|
|
1791
|
+
async function restartWorkerSession(cwd, workerName) {
|
|
1792
|
+
// Find workspace root
|
|
1793
|
+
let workspaceRoot = null;
|
|
1794
|
+
let dir = cwd;
|
|
1795
|
+
while (dir !== path.dirname(dir)) {
|
|
1796
|
+
if (fs.existsSync(path.join(dir, WORKSPACE_CONFIG_FILE))) { workspaceRoot = dir; break; }
|
|
1797
|
+
dir = path.dirname(dir);
|
|
1798
|
+
}
|
|
1799
|
+
if (!workspaceRoot) {
|
|
1800
|
+
console.error('Error: Not inside a workspace (no wogi-workspace.json found).');
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
}
|
|
1803
|
+
const config = safeJsonParse(path.join(workspaceRoot, WORKSPACE_CONFIG_FILE), null);
|
|
1804
|
+
const member = config?.channels?.members?.[workerName];
|
|
1805
|
+
if (!member || !member.port) {
|
|
1806
|
+
console.error(`Error: unknown worker "${workerName}". Known workers: ${Object.keys(config?.channels?.members || {}).join(', ') || '(none)'}`);
|
|
1807
|
+
process.exit(1);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const http = require('node:http');
|
|
1811
|
+
const result = await new Promise((resolve) => {
|
|
1812
|
+
const req = http.request({
|
|
1813
|
+
hostname: '127.0.0.1', port: member.port, path: '/restart', method: 'POST',
|
|
1814
|
+
headers: { 'X-Wogi-From': 'manager', 'Content-Length': 0 }, timeout: 5000
|
|
1815
|
+
}, (res) => {
|
|
1816
|
+
let data = '';
|
|
1817
|
+
res.on('data', c => { data += c; });
|
|
1818
|
+
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
1819
|
+
});
|
|
1820
|
+
req.on('error', (err) => resolve({ error: err.message }));
|
|
1821
|
+
req.on('timeout', () => { req.destroy(); resolve({ error: 'timeout' }); });
|
|
1822
|
+
req.end();
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
if (result.error) {
|
|
1826
|
+
console.error(`✗ Could not reach worker "${workerName}" on port ${member.port}: ${result.error}`);
|
|
1827
|
+
console.error(` Is the worker running? Start it with: cd ${member.path || workerName} && flow workspace start`);
|
|
1828
|
+
process.exit(1);
|
|
1829
|
+
}
|
|
1830
|
+
if (result.status === 202) {
|
|
1831
|
+
console.log(`✓ Restart signalled to "${workerName}" (port ${member.port}). The worker will relaunch with fresh code and resume its in-progress task.`);
|
|
1832
|
+
} else {
|
|
1833
|
+
console.error(`✗ Worker "${workerName}" returned HTTP ${result.status}: ${result.body}`);
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1771
1838
|
async function workspace(args) {
|
|
1772
1839
|
const subcommand = args[0];
|
|
1773
1840
|
|
|
@@ -1816,6 +1883,18 @@ async function workspace(args) {
|
|
|
1816
1883
|
startWorkerSession(process.cwd());
|
|
1817
1884
|
break;
|
|
1818
1885
|
}
|
|
1886
|
+
case 'restart': {
|
|
1887
|
+
// S5 (wf-ee87a24e): cycle a worker session so it reloads upgraded code.
|
|
1888
|
+
// POSTs /restart to the worker's channel; the channel server writes the
|
|
1889
|
+
// wrapper flag and SIGTERMs claude → wogi-claude relaunches it fresh.
|
|
1890
|
+
const workerName = args[1];
|
|
1891
|
+
if (!workerName) {
|
|
1892
|
+
console.error('Usage: flow workspace restart <worker>');
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
await restartWorkerSession(process.cwd(), workerName);
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1819
1898
|
default:
|
|
1820
1899
|
console.log(`
|
|
1821
1900
|
Wogi Workspace — Multi-Repo Orchestration
|
|
@@ -1829,12 +1908,14 @@ Commands:
|
|
|
1829
1908
|
add Add a member repo to the workspace
|
|
1830
1909
|
remove Remove a member repo from the workspace
|
|
1831
1910
|
start Start a worker session with channel (run from a member repo)
|
|
1911
|
+
restart Cycle a worker session so it reloads upgraded code + resumes its task
|
|
1832
1912
|
|
|
1833
1913
|
Examples:
|
|
1834
1914
|
flow workspace init # Create workspace from subdirectories
|
|
1835
1915
|
flow workspace sync # Refresh after external changes
|
|
1836
1916
|
flow workspace status # Show all repos, tasks, contracts
|
|
1837
1917
|
cd frontend/ && flow workspace start # Start worker session
|
|
1918
|
+
flow workspace restart backend # Restart the 'backend' worker (after npm upgrade)
|
|
1838
1919
|
`);
|
|
1839
1920
|
}
|
|
1840
1921
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wogiflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.34.2",
|
|
4
4
|
"description": "AI-powered development workflow management system with multi-model support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"flow": "./scripts/flow",
|
|
13
|
-
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js tests/flow-standards-forbidden-patterns.test.js tests/flow-hooks-architect-required-gate.test.js tests/flow-architect-runs.test.js tests/flow-installer-forbidden-patterns.test.js tests/flow-deferral-classifier-ai.test.js tests/flow-no-defer-policy.test.js tests/flow-self-adversary-loop.test.js tests/flow-impl-question-classifier.test.js tests/flow-hooks-self-adversary-gate.test.js tests/flow-scheduled-runner.test.js tests/flow-schedule-cli.test.js tests/flow-skill-portability.test.js tests/flow-skill-export.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
13
|
+
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js tests/flow-standards-forbidden-patterns.test.js tests/flow-hooks-architect-required-gate.test.js tests/flow-architect-runs.test.js tests/flow-installer-forbidden-patterns.test.js tests/flow-deferral-classifier-ai.test.js tests/flow-no-defer-policy.test.js tests/flow-self-adversary-loop.test.js tests/flow-impl-question-classifier.test.js tests/flow-hooks-self-adversary-gate.test.js tests/flow-scheduled-runner.test.js tests/flow-schedule-cli.test.js tests/flow-skill-portability.test.js tests/flow-skill-export.test.js tests/flow-workspace-subtask-state.test.js tests/flow-worker-continuation-gate.test.js tests/flow-workspace-stop-notify.test.js tests/flow-workspace-channel-status.test.js tests/flow-workspace-restart-resume.test.js tests/flow-workspace-sustained-execution.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
14
14
|
"test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
|
|
15
15
|
"lint": "eslint scripts/ lib/ tests/",
|
|
16
16
|
"lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
|
package/scripts/flow
CHANGED
|
@@ -381,6 +381,23 @@ case "${1:-}" in
|
|
|
381
381
|
ready)
|
|
382
382
|
node "$SCRIPT_DIR/flow-ready.js" "${@:2}"
|
|
383
383
|
;;
|
|
384
|
+
# F11 (R-379): bin/flow (npm-installed dispatcher) had `schedule` wired
|
|
385
|
+
# at line 232, but scripts/flow (dev/repo dispatcher) did not — so
|
|
386
|
+
# `flow schedule install` failed silently inside the dev repo. Wire it
|
|
387
|
+
# here so both dispatchers stay in sync. Same for `skill export`, which
|
|
388
|
+
# ships in v2.33.0 but was only reachable from the npm-installed path.
|
|
389
|
+
schedule)
|
|
390
|
+
node "$SCRIPT_DIR/flow-schedule.js" "${@:2}"
|
|
391
|
+
;;
|
|
392
|
+
skill|skills)
|
|
393
|
+
# Sub-subcommand: `flow skill export ...` → flow-skill-export.js
|
|
394
|
+
# Other `flow skill ...` subcommands → flow-skill-manage.js (existing).
|
|
395
|
+
if [ "${2:-}" = "export" ]; then
|
|
396
|
+
node "$SCRIPT_DIR/flow-skill-export.js" "${@:3}"
|
|
397
|
+
else
|
|
398
|
+
node "$SCRIPT_DIR/flow-skill-manage.js" "${@:2}"
|
|
399
|
+
fi
|
|
400
|
+
;;
|
|
384
401
|
start)
|
|
385
402
|
node "$SCRIPT_DIR/flow-start.js" "${@:2}"
|
|
386
403
|
;;
|
|
@@ -148,7 +148,9 @@ const KNOWN_CONFIG_KEYS = [
|
|
|
148
148
|
// Task-boundary session restart (wf-39e9dc09) — opt-in, experimental
|
|
149
149
|
'taskBoundaryReset',
|
|
150
150
|
// Session hydration recency filter (wf-729ab5c0)
|
|
151
|
-
'sessionHydration'
|
|
151
|
+
'sessionHydration',
|
|
152
|
+
// Workspace (multi-repo manager/worker) — epic-workspace-sustained-exec
|
|
153
|
+
'workspace'
|
|
152
154
|
];
|
|
153
155
|
|
|
154
156
|
module.exports = {
|
package/scripts/flow-io.js
CHANGED
|
@@ -607,6 +607,23 @@ async function acquireLock(filePath, options = {}) {
|
|
|
607
607
|
await require('node:timers/promises').setTimeout(delay);
|
|
608
608
|
continue;
|
|
609
609
|
}
|
|
610
|
+
} else if (err.code === 'ENOENT' && attempt < retries) {
|
|
611
|
+
// Race: a concurrent cleanup (another process's release() or
|
|
612
|
+
// cleanupStaleLocks) removed the lock dir between our mkdirSync and the
|
|
613
|
+
// info.json write, so writeFileSync(lockInfoFile) failed ENOENT. This is
|
|
614
|
+
// transient — retry rather than failing hard. (Pre-existing flaky-lock
|
|
615
|
+
// root cause surfaced by parallel test runs; wf-0381b27b.)
|
|
616
|
+
//
|
|
617
|
+
// Remove only our EMPTY orphan dir if one lingers: rmdirSync succeeds
|
|
618
|
+
// only when the dir is empty, so we never clobber another holder's lock
|
|
619
|
+
// (its info.json would make this ENOTEMPTY → we leave it for the normal
|
|
620
|
+
// EEXIST/stale path on the next attempt).
|
|
621
|
+
try { fs.rmdirSync(lockDir); } catch (_e) { /* gone, or now held by another */ }
|
|
622
|
+
const delay = exponentialBackoff
|
|
623
|
+
? retryDelay * Math.pow(2, attempt)
|
|
624
|
+
: retryDelay * (attempt + 1);
|
|
625
|
+
await require('node:timers/promises').setTimeout(delay);
|
|
626
|
+
continue;
|
|
610
627
|
}
|
|
611
628
|
|
|
612
629
|
throw new Error(`Failed to acquire lock for ${filePath}: ${err.message}`, { cause: err });
|
package/scripts/flow-paths.js
CHANGED
|
@@ -64,6 +64,85 @@ function getProjectRoot() {
|
|
|
64
64
|
return process.cwd();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Worktree-stable canonical resolution (wf-e5e57361 / RC2)
|
|
69
|
+
// ============================================================
|
|
70
|
+
//
|
|
71
|
+
// `git rev-parse --show-toplevel` is NOT worktree-stable: from inside a linked
|
|
72
|
+
// git worktree it returns the WORKTREE root, not the main repository root. State
|
|
73
|
+
// files under `.workflow/state/` are gitignored, so a worktree never carries them
|
|
74
|
+
// (e.g. `workflow-phase.json`). That made the phase gates fail-open to an
|
|
75
|
+
// unrestricted "idle" phase when a process ran from a worktree — an "ungated
|
|
76
|
+
// context" a worker could reach by `git worktree add`. The fix: resolve gate
|
|
77
|
+
// state from the CANONICAL (main-repo) location via `--git-common-dir`, which IS
|
|
78
|
+
// worktree-stable (it always points at the main repo's `.git`).
|
|
79
|
+
//
|
|
80
|
+
// Both values come from a single `git rev-parse` call, memoized per-process.
|
|
81
|
+
|
|
82
|
+
let _gitInfo; // { topLevel, mainRoot } | null
|
|
83
|
+
function resolveGitInfo() {
|
|
84
|
+
if (_gitInfo !== undefined) return _gitInfo;
|
|
85
|
+
try {
|
|
86
|
+
// One call returns two lines: show-toplevel, then git-common-dir (absolute).
|
|
87
|
+
const out = execSync('git rev-parse --path-format=absolute --show-toplevel --git-common-dir', {
|
|
88
|
+
encoding: 'utf-8',
|
|
89
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
90
|
+
}).trim();
|
|
91
|
+
const [topLevel, commonDir] = out.split('\n').map(s => s.trim());
|
|
92
|
+
if (topLevel && commonDir) {
|
|
93
|
+
// commonDir is `<mainRoot>/.git` in both the main tree and any worktree.
|
|
94
|
+
_gitInfo = { topLevel: path.resolve(topLevel), mainRoot: path.dirname(path.resolve(commonDir)) };
|
|
95
|
+
return _gitInfo;
|
|
96
|
+
}
|
|
97
|
+
} catch (_err) { /* not a git repo / git unavailable */ }
|
|
98
|
+
_gitInfo = null;
|
|
99
|
+
return _gitInfo;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the canonical (main-repo) `.workflow/state` directory, stable across
|
|
104
|
+
* git worktrees. Falls back to the cwd-relative STATE_DIR when git is
|
|
105
|
+
* unavailable or the canonical state dir doesn't exist (no regression vs prior
|
|
106
|
+
* behavior). An explicit `WOGI_CANONICAL_STATE_DIR` env var overrides everything
|
|
107
|
+
* (used by tests and by callers that already resolved it).
|
|
108
|
+
*
|
|
109
|
+
* @returns {string} Absolute path to the canonical `.workflow/state` directory
|
|
110
|
+
*/
|
|
111
|
+
let _canonicalStateDir; // memo
|
|
112
|
+
function getCanonicalStateDir() {
|
|
113
|
+
if (_canonicalStateDir !== undefined) return _canonicalStateDir;
|
|
114
|
+
if (process.env.WOGI_CANONICAL_STATE_DIR && fs.existsSync(process.env.WOGI_CANONICAL_STATE_DIR)) {
|
|
115
|
+
_canonicalStateDir = process.env.WOGI_CANONICAL_STATE_DIR;
|
|
116
|
+
return _canonicalStateDir;
|
|
117
|
+
}
|
|
118
|
+
const info = resolveGitInfo();
|
|
119
|
+
if (info && info.mainRoot) {
|
|
120
|
+
const candidate = path.join(info.mainRoot, '.workflow', 'state');
|
|
121
|
+
if (fs.existsSync(candidate)) {
|
|
122
|
+
_canonicalStateDir = candidate;
|
|
123
|
+
return _canonicalStateDir;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
_canonicalStateDir = STATE_DIR; // fallback: cwd-relative (current behavior)
|
|
127
|
+
return _canonicalStateDir;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* True when the current working directory is inside a LINKED git worktree
|
|
132
|
+
* (i.e. the working-tree top-level differs from the main repository root). Used
|
|
133
|
+
* to fail gates CLOSED for in-progress tasks rather than fail-open to "idle"
|
|
134
|
+
* when phase state cannot be resolved. Returns false outside git or on the main
|
|
135
|
+
* working tree.
|
|
136
|
+
*
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
function isLinkedWorktree() {
|
|
140
|
+
if (process.env.WOGI_FORCE_WORKTREE === '1') return true; // test seam
|
|
141
|
+
const info = resolveGitInfo();
|
|
142
|
+
if (!info) return false;
|
|
143
|
+
return info.topLevel !== info.mainRoot;
|
|
144
|
+
}
|
|
145
|
+
|
|
67
146
|
// ============================================================
|
|
68
147
|
// Package Root (where wogiflow npm package lives)
|
|
69
148
|
// ============================================================
|
|
@@ -283,6 +362,8 @@ function checkSpecMigration() {
|
|
|
283
362
|
|
|
284
363
|
module.exports = {
|
|
285
364
|
getProjectRoot,
|
|
365
|
+
getCanonicalStateDir,
|
|
366
|
+
isLinkedWorktree,
|
|
286
367
|
PROJECT_ROOT,
|
|
287
368
|
PACKAGE_ROOT,
|
|
288
369
|
PACKAGE_PATHS,
|
package/scripts/flow-schedule.js
CHANGED
|
@@ -100,6 +100,15 @@ function nodeBinary() {
|
|
|
100
100
|
// Unit content generators (pure — no I/O)
|
|
101
101
|
// ============================================================
|
|
102
102
|
|
|
103
|
+
// F8 (R-379): paths that get inlined into shell-interpreted contexts
|
|
104
|
+
// (crontab lines, systemd ExecStart) MUST be single-quoted with embedded
|
|
105
|
+
// single-quotes escaped. Without this, a project at `/Users/Alice Smith/...`
|
|
106
|
+
// breaks the schedule because cron treats the space as an argument boundary.
|
|
107
|
+
function shellQuote(s) {
|
|
108
|
+
// POSIX single-quote escape: replace each ' with '\''
|
|
109
|
+
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
function generateCrontabLines(opts = {}) {
|
|
104
113
|
const node = opts.node || nodeBinary();
|
|
105
114
|
const runner = opts.runner || runnerScriptPath();
|
|
@@ -109,9 +118,11 @@ function generateCrontabLines(opts = {}) {
|
|
|
109
118
|
];
|
|
110
119
|
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
111
120
|
const spec = SCHEDULES[jobName];
|
|
121
|
+
const logPath = path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.log`);
|
|
112
122
|
lines.push(
|
|
113
|
-
`${spec.cronExpr} cd ${projectRoot} &&
|
|
114
|
-
|
|
123
|
+
`${spec.cronExpr} cd ${shellQuote(projectRoot)} && ` +
|
|
124
|
+
`${shellQuote(node)} ${shellQuote(runner)} ${jobName} ` +
|
|
125
|
+
`>> ${shellQuote(logPath)} 2>&1`
|
|
115
126
|
);
|
|
116
127
|
}
|
|
117
128
|
lines.push(`# === wogi-scheduled end ===`);
|
|
@@ -172,14 +183,18 @@ function generateSystemdServiceUnit(jobName, opts = {}) {
|
|
|
172
183
|
const projectRoot = opts.projectRoot || repoRoot();
|
|
173
184
|
if (!SCHEDULES[jobName]) throw new Error(`No schedule defined for "${jobName}"`);
|
|
174
185
|
|
|
186
|
+
// F8 (R-379): systemd ExecStart with a path containing spaces needs each
|
|
187
|
+
// arg surrounded by quotes — systemd's argument parser splits on spaces.
|
|
188
|
+
// WorkingDirectory and Standard{Output,Error} accept literal paths (no
|
|
189
|
+
// splitting), but quoting them too is harmless and consistent.
|
|
175
190
|
return `[Unit]
|
|
176
191
|
Description=Wogi Flow scheduled job: ${jobName}
|
|
177
192
|
After=network-online.target
|
|
178
193
|
|
|
179
194
|
[Service]
|
|
180
195
|
Type=oneshot
|
|
181
|
-
WorkingDirectory
|
|
182
|
-
ExecStart
|
|
196
|
+
WorkingDirectory="${projectRoot}"
|
|
197
|
+
ExecStart="${node}" "${runner}" "${jobName}"
|
|
183
198
|
StandardOutput=append:${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.log`)}
|
|
184
199
|
StandardError=append:${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.err.log`)}
|
|
185
200
|
`;
|
|
@@ -361,12 +376,14 @@ function getStatus(deps = {}) {
|
|
|
361
376
|
const cfrag = path.join(homeDir, '.config', 'wogi-flow', 'crontab-fragment');
|
|
362
377
|
if (fsx.existsSync(cfrag)) status.cron.push(cfrag);
|
|
363
378
|
|
|
364
|
-
// systemd
|
|
379
|
+
// systemd — both .service and .timer files are tracked under the same key.
|
|
380
|
+
// F21 (R-379): the prior ternary `ext === 'timer' ? 'systemd' : 'systemd'`
|
|
381
|
+
// had identical branches — a meaningless conditional. Just push directly.
|
|
365
382
|
const sdir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
366
383
|
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
367
384
|
for (const ext of ['service', 'timer']) {
|
|
368
385
|
const p = path.join(sdir, `wogi-scheduled-${jobName}.${ext}`);
|
|
369
|
-
if (fsx.existsSync(p)) status
|
|
386
|
+
if (fsx.existsSync(p)) status.systemd.push(p);
|
|
370
387
|
}
|
|
371
388
|
}
|
|
372
389
|
return status;
|