@brainervirus/workit-core 0.8.1 → 0.8.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/package.json +1 -1
- package/src/core/detector.ts +65 -44
- package/src/core/docs-layout.ts +38 -10
- package/src/core/flow-state.ts +1133 -132
- package/src/core/handoff-context.ts +8 -0
- package/src/core/handoff-tools.ts +21 -1
- package/src/core/menu.ts +68 -0
- package/src/core/reminder.ts +31 -1
- package/src/core/sdd.ts +57 -0
- package/templates/execution-contract.md +11 -0
- package/templates/superpowers-doc-contract.md +2 -0
package/package.json
CHANGED
package/src/core/detector.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CONFIG_GAP_MARKER } from "./config-guard";
|
|
4
|
-
import {
|
|
4
|
+
import { readEffectiveFlowState, type FlowReadResult } from "./flow-state";
|
|
5
5
|
|
|
6
6
|
export type Detection = { choices: string[]; pattern: "alpha" | "numeric" } | null;
|
|
7
7
|
|
|
@@ -162,57 +162,78 @@ export const detectBacktickDocRefs = (text: string): string[] | null => {
|
|
|
162
162
|
return refs;
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
-
// The rail
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const completeIds = new Set(
|
|
187
|
-
taskLines.map((l) => Number(/^Task\s*(\d+):/i.exec(l)?.[1])).filter(Number.isFinite),
|
|
188
|
-
);
|
|
189
|
-
return planTasks.every((t) => completeIds.has(t.id));
|
|
190
|
-
} catch {
|
|
191
|
-
return false; // unreadable/missing plan.md — not provably complete, rail stays on
|
|
192
|
-
}
|
|
165
|
+
// The rail is active-plan detection (CA-11/CA-13): only a flow whose effective
|
|
166
|
+
// execution is ACTIVE and subagent-driven is found. The effective read performs
|
|
167
|
+
// the legacy compatibility migration (ledgerCompletion-derived, CA-16) and the
|
|
168
|
+
// approval-digest reconciliation and PERSISTS any drift/migration reset, so a
|
|
169
|
+
// drift-reset or pending/paused/completed flow is excluded after its state is
|
|
170
|
+
// rewritten on read; only malformed/unreadable flow.json is excluded without
|
|
171
|
+
// ever being rewritten (readFlowStrict rejects it before any write).
|
|
172
|
+
export type ActivePlanScan = {
|
|
173
|
+
slugs: string[];
|
|
174
|
+
/**
|
|
175
|
+
* Flows whose effective read FAILED with a transient lock/IO error
|
|
176
|
+
* (flow_concurrent_conflict / flow_io_error) — a held lock or a filesystem
|
|
177
|
+
* hiccup, NOT "not active". `findActiveSubagentDrivenPlans` still excludes
|
|
178
|
+
* them (its `string[]` contract cannot express a read failure, so the
|
|
179
|
+
* plugin rail stays fail-open by design); this signal lets a caller that
|
|
180
|
+
* wants fail-closed behavior treat a non-empty list as "the plan state is
|
|
181
|
+
* unknown". The coordinator's authoritative product gate
|
|
182
|
+
* (assertProductGates) fails closed regardless, so legitimate interception
|
|
183
|
+
* is never weakened.
|
|
184
|
+
*/
|
|
185
|
+
read_errors: { slug: string; code: string; error: string }[];
|
|
193
186
|
};
|
|
194
187
|
|
|
195
|
-
export const
|
|
188
|
+
export const scanActiveSubagentDrivenPlans = (root: string): ActivePlanScan => {
|
|
196
189
|
const docsDir = path.join(root, "docs");
|
|
197
|
-
if (!existsSync(docsDir)) return [];
|
|
190
|
+
if (!existsSync(docsDir)) return { slugs: [], read_errors: [] };
|
|
198
191
|
const slugs: string[] = [];
|
|
192
|
+
const readErrors: { slug: string; code: string; error: string }[] = [];
|
|
193
|
+
// The full effective read (lock + digest hashing + possibly writing drift
|
|
194
|
+
// resets) is the expensive path; it runs per slug on every message/tool call.
|
|
195
|
+
const classify = (slug: string) => {
|
|
196
|
+
let effective: FlowReadResult;
|
|
197
|
+
try {
|
|
198
|
+
effective = readEffectiveFlowState(root, slug);
|
|
199
|
+
} catch {
|
|
200
|
+
// an unexpected read error (e.g. a non-slug directory name) excludes the
|
|
201
|
+
// entry without touching it
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!effective.ok) {
|
|
205
|
+
if (effective.code === "flow_concurrent_conflict" || effective.code === "flow_io_error") {
|
|
206
|
+
readErrors.push({ slug, code: effective.code, error: effective.error });
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const exec = effective.state.execution;
|
|
211
|
+
if (exec.status === "active" && exec.mode === "subagent-driven") slugs.push(slug);
|
|
212
|
+
};
|
|
199
213
|
for (const slug of readdirSync(docsDir)) {
|
|
200
214
|
const file = path.join(docsDir, slug, "sdd", "flow.json");
|
|
215
|
+
if (!existsSync(file)) continue;
|
|
216
|
+
// Raw pre-filter (Task 2 advisory): a flow.json that cannot plausibly hold
|
|
217
|
+
// an active subagent-driven execution is skipped with ONE cheap read.
|
|
218
|
+
// Absence of the `subagent-driven` token makes an ACTIVE subagent-driven
|
|
219
|
+
// execution impossible — an explicit execution requires mode
|
|
220
|
+
// "subagent-driven" and legacy derivation (CA-16) requires menu.chosen
|
|
221
|
+
// "subagent-driven", both embedding the literal token. The pre-filter can
|
|
222
|
+
// only skip and never causes a false negative: an unreadable file or one
|
|
223
|
+
// containing a backslash-u escape (hand-encoded, never produced by the
|
|
224
|
+
// toolkit's own writers) is treated as in-doubt and runs the full read.
|
|
225
|
+
let raw: string;
|
|
201
226
|
try {
|
|
202
|
-
|
|
203
|
-
menu?: { chosen?: string };
|
|
204
|
-
plan?: { status?: string };
|
|
205
|
-
};
|
|
206
|
-
if (
|
|
207
|
-
flow.menu?.chosen === "subagent-driven" &&
|
|
208
|
-
flow.plan?.status === "approved" &&
|
|
209
|
-
!isPlanComplete(path.join(docsDir, slug))
|
|
210
|
-
) {
|
|
211
|
-
slugs.push(slug);
|
|
212
|
-
}
|
|
227
|
+
raw = readFileSync(file, "utf8");
|
|
213
228
|
} catch {
|
|
214
|
-
|
|
229
|
+
classify(slug);
|
|
230
|
+
continue;
|
|
215
231
|
}
|
|
232
|
+
if (!raw.includes("subagent-driven") && !raw.includes("\\u")) continue;
|
|
233
|
+
classify(slug);
|
|
216
234
|
}
|
|
217
|
-
return slugs;
|
|
235
|
+
return { slugs, read_errors: readErrors };
|
|
218
236
|
};
|
|
237
|
+
|
|
238
|
+
export const findActiveSubagentDrivenPlans = (root: string): string[] =>
|
|
239
|
+
scanActiveSubagentDrivenPlans(root).slugs;
|
package/src/core/docs-layout.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
// One canonical document path contract (DC-01, DC-02, DC-04, DC-14): workspace
|
|
@@ -49,7 +49,22 @@ const canonicalize = (base: string, candidate: string): string => {
|
|
|
49
49
|
const abs = path.resolve(base, candidate);
|
|
50
50
|
let ancestor = abs;
|
|
51
51
|
while (!existsSync(ancestor)) ancestor = path.dirname(ancestor);
|
|
52
|
-
|
|
52
|
+
let real: string;
|
|
53
|
+
try {
|
|
54
|
+
real = realpathSync(ancestor);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// macOS realpath fails with EACCES on a mode-000 file while Linux succeeds;
|
|
57
|
+
// an existing non-symlink file cannot escape the workspace, so resolve the
|
|
58
|
+
// nearest existing directory instead (symlinks keep the strict path).
|
|
59
|
+
if (
|
|
60
|
+
(error as NodeJS.ErrnoException).code === "EACCES" &&
|
|
61
|
+
!lstatSync(ancestor).isSymbolicLink()
|
|
62
|
+
) {
|
|
63
|
+
real = path.join(realpathSync(path.dirname(ancestor)), path.basename(ancestor));
|
|
64
|
+
} else {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
53
68
|
if (real !== base && !real.startsWith(base + path.sep)) {
|
|
54
69
|
throw new Error(`path must stay inside repository root: ${candidate}`);
|
|
55
70
|
}
|
|
@@ -106,14 +121,12 @@ export const resolveCanonicalLayout = (input: {
|
|
|
106
121
|
if (path.isAbsolute(candidate)) {
|
|
107
122
|
return { ok: false, error: `absolute path not allowed: ${candidate}` };
|
|
108
123
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const rel = posix(path.relative(workspace, abs));
|
|
116
|
-
const match = rel.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
|
|
124
|
+
// Exact-spelling contract (DC-01): the caller path must be written as the
|
|
125
|
+
// canonical `docs/<slug>/spec.md` / `docs/<slug>/plan.md` — no `./`,
|
|
126
|
+
// no `..` segments, no repeated or trailing separators. The strict regex
|
|
127
|
+
// below rejects those spellings before any bytes are read or resolved.
|
|
128
|
+
const spelling = posix(candidate);
|
|
129
|
+
const match = spelling.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
|
|
117
130
|
if (!match) {
|
|
118
131
|
return {
|
|
119
132
|
ok: false,
|
|
@@ -140,6 +153,21 @@ export const resolveCanonicalLayout = (input: {
|
|
|
140
153
|
};
|
|
141
154
|
}
|
|
142
155
|
derived = pathSlug;
|
|
156
|
+
// Symlink/canonical containment (DC-02): after the exact-spelling match,
|
|
157
|
+
// resolve the canonical path so a symlinked docs/<slug> or doc file that
|
|
158
|
+
// escapes the workspace or resolves to a different slug is still rejected.
|
|
159
|
+
let abs: string;
|
|
160
|
+
try {
|
|
161
|
+
abs = canonicalize(workspace, candidate);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
164
|
+
}
|
|
165
|
+
if (posix(path.relative(workspace, abs)) !== spelling) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
error: `path must resolve to ${JSON.stringify(spelling)}: ${candidate}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
143
171
|
}
|
|
144
172
|
|
|
145
173
|
let resolvedSlug = slug;
|