@brainervirus/workit-core 0.8.1 → 0.8.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "private": false,
5
5
  "description": "Workit — workflow rails for agentic coding: specs, plans, YouTrack, CI-gated commits (shared core)",
6
6
  "keywords": [
@@ -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 { parseTasksFromPlan } from "./docs-validate";
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 has no terminal FlowStatus a fully completed SDD ledger is done,
166
- // but only if its complete set covers every task id in the plan (the last
167
- // task's append may have been skipped, leaving an all-complete partial ledger).
168
- const isPlanComplete = (slugDir: string): boolean => {
169
- const ledger = path.join(slugDir, "sdd", "progress.md");
170
- if (!existsSync(ledger)) return false; // no ledger yet not provably complete
171
- let taskLines: string[];
172
- try {
173
- taskLines = readFileSync(ledger, "utf8")
174
- .split("\n")
175
- .map((l) => l.trim())
176
- .filter((l) => /^Task \s*\d+:/i.test(l));
177
- } catch {
178
- return true; // unreadable ledger exclude the slug
179
- }
180
- if (taskLines.length === 0 || !taskLines.every((l) => /^Task \s*\d+:\s*complete\b/i.test(l))) {
181
- return false;
182
- }
183
- try {
184
- const planTasks = parseTasksFromPlan(readFileSync(path.join(slugDir, "plan.md"), "utf8"));
185
- if (planTasks.length === 0) return true;
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 findActiveSubagentDrivenPlans = (root: string): string[] => {
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
- const flow = JSON.parse(readFileSync(file, "utf8")) as {
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
- // skip unreadable or malformed flow.json
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;
@@ -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
- const real = realpathSync(ancestor);
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
- let abs: string;
110
- try {
111
- abs = canonicalize(workspace, candidate);
112
- } catch (error) {
113
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
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;