@loomhelm/multi-task-flow 0.2.6 → 0.2.7

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 (93) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/toolkit/artifact-manifest.json +330 -26
  4. package/toolkit/package.json +1 -1
  5. package/toolkit/skills/docs-migrate/scripts/_runtime/compatibility_cli.ts +12 -0
  6. package/toolkit/skills/docs-migrate/scripts/_runtime/compatibility_docs.ts +115 -0
  7. package/toolkit/skills/docs-migrate/scripts/_runtime/compatibility_shared.ts +230 -0
  8. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/checks/replay.ts +336 -0
  9. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/git.ts +54 -0
  10. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
  11. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
  12. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
  13. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/migration_common.ts +291 -0
  14. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/next_plan.ts +199 -0
  15. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
  16. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
  17. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
  18. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
  19. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
  20. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
  21. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
  22. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
  23. package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
  24. package/toolkit/skills/docs-migrate/scripts/gen_legacy_spec_id.ts +1 -1
  25. package/toolkit/skills/docs-migrate/scripts/specs_migrate_guard.ts +1 -1
  26. package/toolkit/skills/find-next-plan/scripts/_runtime/compatibility_cli.ts +12 -0
  27. package/toolkit/skills/find-next-plan/scripts/_runtime/compatibility_docs.ts +115 -0
  28. package/toolkit/skills/find-next-plan/scripts/_runtime/compatibility_shared.ts +230 -0
  29. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/checks/replay.ts +336 -0
  30. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/git.ts +54 -0
  31. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
  32. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
  33. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
  34. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/migration_common.ts +291 -0
  35. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/next_plan.ts +199 -0
  36. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
  37. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
  38. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
  39. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
  40. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
  41. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
  42. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
  43. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
  44. package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
  45. package/toolkit/skills/find-next-plan/scripts/find_next_plan.ts +1 -1
  46. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/compatibility_cli.ts +12 -0
  47. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/compatibility_docs.ts +115 -0
  48. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/compatibility_shared.ts +230 -0
  49. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/checks/replay.ts +336 -0
  50. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/git.ts +54 -0
  51. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
  52. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
  53. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
  54. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/migration_common.ts +291 -0
  55. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/next_plan.ts +199 -0
  56. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
  57. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
  58. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
  59. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
  60. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
  61. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
  62. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
  63. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
  64. package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
  65. package/toolkit/skills/hardware-doc-ingest/scripts/ingest_mineru_extract.ts +1 -1
  66. package/toolkit/skills/hardware-doc-ingest/scripts/verify_doc_locks.ts +1 -1
  67. package/toolkit/skills/multi-task-flow/dist/app.js +66 -66
  68. package/toolkit/skills/multi-task-flow/lib/view.ts +6 -2
  69. package/toolkit/skills/multi-task-flow/web/app.ts +2 -2
  70. package/toolkit/skills/multi-task-flow/web/graph-layout.ts +3 -3
  71. package/toolkit/skills/source-manifest.json +315 -11
  72. package/toolkit/skills/spec-sync/SKILL.md +1 -1
  73. package/toolkit/skills/spec-sync/scripts/_runtime/compatibility_cli.ts +12 -0
  74. package/toolkit/skills/spec-sync/scripts/_runtime/compatibility_docs.ts +115 -0
  75. package/toolkit/skills/spec-sync/scripts/_runtime/compatibility_shared.ts +230 -0
  76. package/toolkit/skills/spec-sync/scripts/_runtime/modules/checks/replay.ts +336 -0
  77. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/git.ts +54 -0
  78. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
  79. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
  80. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
  81. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/migration_common.ts +291 -0
  82. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/next_plan.ts +199 -0
  83. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
  84. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
  85. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
  86. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
  87. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
  88. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
  89. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
  90. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
  91. package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
  92. package/toolkit/skills/spec-sync/scripts/spec_drift_check.ts +1 -1
  93. package/toolkit/skills/support-manifest.json +77 -1
@@ -0,0 +1,291 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { gitText } from "./git";
4
+
5
+ export const PREFIXED_NAME = /^(?<legacyId>[a-z0-9]{4,5})[-:](?<topic>.+)$/i;
6
+ const TOPIC_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7
+ const INLINE_LINK = /\]\(\s*(?:<(?<angle>[^>]+)>|(?<bare>[^\s)]+))/gm;
8
+ const REFERENCE_LINK =
9
+ /^\s*\[[^\]]+\]:\s*(?:<(?<angle>[^>]+)>|(?<bare>[^\s]+))/gm;
10
+ const CATALOG_LINK =
11
+ /^\s*\[[^\]]*\]\(\s*(?:<(?<angle>[^>]+)>|(?<bare>[^\s)]+))(?:\s+["'][^"']*["'])?\s*\)\s*$/;
12
+
13
+ export type Json = Record<string, unknown>;
14
+
15
+ export function relativePath(root: string, path: string): string {
16
+ const value = relative(resolve(root), resolve(path)).replaceAll("\\", "/");
17
+ if (value === "" || value === ".") return ".";
18
+ return value;
19
+ }
20
+
21
+ export function parsePrefixedName(name: string): [string, string] | null {
22
+ const match = PREFIXED_NAME.exec(name);
23
+ return match?.groups?.legacyId && match.groups.topic
24
+ ? [match.groups.legacyId, match.groups.topic]
25
+ : null;
26
+ }
27
+
28
+ export function isTopicSlug(value: string): boolean {
29
+ return TOPIC_SLUG.test(value);
30
+ }
31
+
32
+ export function tableRows(text: string): Json[] {
33
+ const rows: Json[] = [];
34
+ const lines = text.split(/\r?\n/);
35
+ for (let index = 0; index + 1 < lines.length;) {
36
+ const header = lines[index] as string;
37
+ const separator = lines[index + 1] as string;
38
+ if (
39
+ !header.trimStart().startsWith("|") ||
40
+ !separator.trimStart().startsWith("|")
41
+ ) {
42
+ index += 1;
43
+ continue;
44
+ }
45
+ const headers = header
46
+ .trim()
47
+ .replace(/^\||\|$/g, "")
48
+ .split("|")
49
+ .map((cell) => cell.trim().toLowerCase());
50
+ const separators = separator
51
+ .trim()
52
+ .replace(/^\||\|$/g, "")
53
+ .split("|")
54
+ .map((cell) => cell.trim());
55
+ if (
56
+ !headers.length ||
57
+ headers.length !== separators.length ||
58
+ separators.some((cell) => !/^:?-{3,}:?$/.test(cell))
59
+ ) {
60
+ index += 1;
61
+ continue;
62
+ }
63
+ index += 2;
64
+ while (
65
+ index < lines.length &&
66
+ (lines[index] as string).trimStart().startsWith("|")
67
+ ) {
68
+ const values = (lines[index] as string)
69
+ .trim()
70
+ .replace(/^\||\|$/g, "")
71
+ .split("|")
72
+ .map((cell) => cell.trim());
73
+ if (values.length === headers.length) {
74
+ const row: Json = {};
75
+ headers.forEach((key, position) => {
76
+ row[key] = values[position] ?? "";
77
+ });
78
+ rows.push(row);
79
+ }
80
+ index += 1;
81
+ }
82
+ }
83
+ return rows;
84
+ }
85
+
86
+ function unwrapCatalogPath(value: string): string {
87
+ let candidate = value.trim();
88
+ if (candidate.startsWith("`") && candidate.endsWith("`"))
89
+ candidate = candidate.slice(1, -1).trim();
90
+ const match = CATALOG_LINK.exec(candidate);
91
+ return match?.groups?.angle ?? match?.groups?.bare ?? candidate;
92
+ }
93
+
94
+ export function normalizeRepoRelativePath(value: string): string | null {
95
+ const withoutFragment = decodeURIComponent(
96
+ value.split("#", 1)[0]?.split("?", 1)[0] ?? "",
97
+ ).trim();
98
+ if (
99
+ !withoutFragment ||
100
+ withoutFragment.startsWith("/") ||
101
+ withoutFragment.startsWith("#") ||
102
+ withoutFragment.includes("://")
103
+ )
104
+ return null;
105
+ const parts: string[] = [];
106
+ for (const part of withoutFragment.split("/")) {
107
+ if (!part || part === ".") continue;
108
+ if (part === "..") {
109
+ if (!parts.length) return null;
110
+ parts.pop();
111
+ } else parts.push(part);
112
+ }
113
+ return parts.join("/") || null;
114
+ }
115
+
116
+ function catalogPathCandidates(
117
+ value: string,
118
+ catalogRelative: string,
119
+ ): Set<string> {
120
+ const path = unwrapCatalogPath(value);
121
+ const direct = normalizeRepoRelativePath(path);
122
+ const catalogRoot = normalizeRepoRelativePath(
123
+ dirname(catalogRelative).replaceAll("\\", "/"),
124
+ );
125
+ const relativePathValue = catalogRoot
126
+ ? normalizeRepoRelativePath(`${catalogRoot}/${path}`)
127
+ : null;
128
+ return new Set(
129
+ [direct, relativePathValue].filter(
130
+ (candidate): candidate is string => candidate !== null,
131
+ ),
132
+ );
133
+ }
134
+
135
+ function catalogTexts(repoRoot: string, catalogRelative: string): string[] {
136
+ const texts: string[] = [];
137
+ const path = resolve(repoRoot, catalogRelative);
138
+ if (existsSync(path)) texts.push(readFileSync(path, "utf8"));
139
+ const history = gitText(repoRoot, [
140
+ "log",
141
+ "--format=%H",
142
+ "HEAD",
143
+ "--",
144
+ catalogRelative,
145
+ ]);
146
+ for (const revision of history?.split(/\r?\n/).filter(Boolean) ?? []) {
147
+ const text = gitText(repoRoot, ["show", `${revision}:${catalogRelative}`]);
148
+ if (text !== null && !texts.includes(text)) texts.push(text);
149
+ }
150
+ return texts;
151
+ }
152
+
153
+ export function catalogConfirmsLegacyId(
154
+ repoRoot: string,
155
+ catalogRelative: string,
156
+ sourceRelative: string,
157
+ legacyId: string,
158
+ ): boolean {
159
+ const expected = normalizeRepoRelativePath(sourceRelative);
160
+ if (!expected) return false;
161
+ return catalogTexts(repoRoot, catalogRelative).some((text) =>
162
+ tableRows(text).some((row) => {
163
+ if (
164
+ String(row.id ?? "")
165
+ .replaceAll("`", "")
166
+ .toLowerCase() !== legacyId.toLowerCase()
167
+ )
168
+ return false;
169
+ return ["spec", "plan", "path"].some((column) =>
170
+ catalogPathCandidates(String(row[column] ?? ""), catalogRelative).has(
171
+ expected,
172
+ ),
173
+ );
174
+ }),
175
+ );
176
+ }
177
+
178
+ export function legacyIdentity(
179
+ repoRoot: string,
180
+ name: string,
181
+ catalogRelative: string,
182
+ sourceRelative: string,
183
+ explicit?: string,
184
+ ): [string, string, string] | null {
185
+ const parsed = parsePrefixedName(name);
186
+ if (!parsed) return null;
187
+ const [legacyId, topic] = parsed;
188
+ if (explicit !== undefined) {
189
+ if (explicit.toLowerCase() !== legacyId.toLowerCase())
190
+ throw new Error(
191
+ `--legacy-id '${explicit}' does not match source prefix '${legacyId}'`,
192
+ );
193
+ return [legacyId, topic, "explicit"];
194
+ }
195
+ return catalogConfirmsLegacyId(
196
+ repoRoot,
197
+ catalogRelative,
198
+ sourceRelative,
199
+ legacyId,
200
+ )
201
+ ? [legacyId, topic, "catalog"]
202
+ : null;
203
+ }
204
+
205
+ function targetIsSource(
206
+ target: string,
207
+ markdownPath: string,
208
+ source: string,
209
+ ): boolean {
210
+ const cleaned = decodeURIComponent(
211
+ target.split("#", 1)[0]?.split("?", 1)[0] ?? "",
212
+ ).trim();
213
+ if (!cleaned || cleaned.includes("://") || cleaned.startsWith("#"))
214
+ return false;
215
+ const resolvedTarget = resolve(dirname(markdownPath), cleaned);
216
+ const sourcePath = resolve(source);
217
+ if (existsSync(sourcePath) && statSafe(sourcePath)?.isFile())
218
+ return resolvedTarget === sourcePath;
219
+ return (
220
+ resolvedTarget === sourcePath || resolvedTarget.startsWith(`${sourcePath}/`)
221
+ );
222
+ }
223
+
224
+ function links(text: string): string[] {
225
+ const result: string[] = [];
226
+ for (const pattern of [INLINE_LINK, REFERENCE_LINK]) {
227
+ pattern.lastIndex = 0;
228
+ for (const match of text.matchAll(pattern))
229
+ result.push(match.groups?.angle ?? match.groups?.bare ?? "");
230
+ }
231
+ return result;
232
+ }
233
+
234
+ function statSafe(path: string): ReturnType<typeof statSync> | null {
235
+ try {
236
+ return statSync(path);
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ export function inboundLinks(
243
+ repoRoot: string,
244
+ source: string,
245
+ sourceRelative: string,
246
+ ): string[] {
247
+ const matches: string[] = [];
248
+ function visit(directory: string): void {
249
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
250
+ const path = resolve(directory, entry.name);
251
+ if (entry.isDirectory()) {
252
+ if (!entry.isSymbolicLink()) visit(path);
253
+ continue;
254
+ }
255
+ if (
256
+ !entry.isFile() ||
257
+ !entry.name.endsWith(".md") ||
258
+ resolve(path) === resolve(source)
259
+ )
260
+ continue;
261
+ const sourceStat = statSafe(source);
262
+ if (
263
+ sourceStat?.isDirectory() &&
264
+ resolve(path).startsWith(`${resolve(source)}/`)
265
+ )
266
+ continue;
267
+ const text = readFileSync(path, "utf8");
268
+ if (
269
+ text.includes(sourceRelative) ||
270
+ links(text).some((target) => targetIsSource(target, path, source))
271
+ )
272
+ matches.push(relativePath(repoRoot, path));
273
+ }
274
+ }
275
+ visit(resolve(repoRoot));
276
+ return matches.sort();
277
+ }
278
+
279
+ export function isProtectedBranch(branch: string): boolean {
280
+ return (
281
+ new Set(["main", "master", "trunk"]).has(branch) ||
282
+ /^(release|hotfix|support)\//.test(branch)
283
+ );
284
+ }
285
+
286
+ export function currentBranch(repoRoot: string): string {
287
+ return (
288
+ gitText(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"])?.trim() ||
289
+ "unknown"
290
+ );
291
+ }
@@ -0,0 +1,199 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import {
4
+ STATUS_DONE,
5
+ STATUS_PARTIAL_PREFIX,
6
+ STATUS_PENDING_DESIGN,
7
+ STATUS_READY,
8
+ STATUS_REDESIGN_PREFIX,
9
+ STATUS_SKIPPED,
10
+ STATUS_VOID,
11
+ createdAt,
12
+ detectRoot,
13
+ noPick,
14
+ relOrAbs,
15
+ resolveCandidate,
16
+ row,
17
+ separator,
18
+ statusRank,
19
+ table,
20
+ type Candidate,
21
+ type Json,
22
+ } from "./next_plan_support";
23
+ export {
24
+ STATUS_DONE,
25
+ STATUS_PARTIAL_PREFIX,
26
+ STATUS_PENDING_DESIGN,
27
+ STATUS_READY,
28
+ STATUS_REDESIGN_PREFIX,
29
+ STATUS_SKIPPED,
30
+ STATUS_VOID,
31
+ } from "./next_plan_support";
32
+
33
+ export function findNextPlan(repoRootInput = "."): Json {
34
+ const root = resolve(repoRootInput);
35
+ const detected = detectRoot(root);
36
+ if (!detected)
37
+ return {
38
+ ok: false,
39
+ reason: "missing_docs_specs_and_plan",
40
+ expected_paths: [
41
+ join(root, "docs/specs/README.md"),
42
+ join(root, "docs/plan/README.md"),
43
+ ],
44
+ };
45
+ const primaryReadme = join(detected.primary, "README.md");
46
+ if (!existsSync(primaryReadme))
47
+ return {
48
+ ok: false,
49
+ reason: "missing_specs_index_readme",
50
+ path: primaryReadme,
51
+ spec_root_mode: detected.mode,
52
+ };
53
+ let lines = readFileSync(primaryReadme, "utf8").split(/\r?\n/);
54
+ let found = table(lines);
55
+ if (!found)
56
+ return {
57
+ ok: false,
58
+ reason: "index_table_not_found",
59
+ path: primaryReadme,
60
+ spec_root_mode: detected.mode,
61
+ };
62
+ let indexRoot = detected.primary;
63
+ let indexSource = "primary";
64
+ if (found.mode === "lifecycle" && detected.fallback) {
65
+ const fallbackReadme = join(detected.fallback, "README.md");
66
+ if (existsSync(fallbackReadme)) {
67
+ const fallbackLines = readFileSync(fallbackReadme, "utf8").split(/\r?\n/);
68
+ const fallbackTable = table(fallbackLines);
69
+ if (fallbackTable?.mode === "status") {
70
+ lines = fallbackLines;
71
+ found = fallbackTable;
72
+ indexRoot = detected.fallback;
73
+ indexSource = "fallback_legacy_plan_queue";
74
+ }
75
+ }
76
+ }
77
+ if (found.mode === "lifecycle") {
78
+ const lifecycles = lines
79
+ .slice(found.start)
80
+ .filter((line) => line.trimStart().startsWith("|"))
81
+ .map((line) => row(line)[found.columns.lifecycle ?? -1])
82
+ .filter((value): value is string => Boolean(value));
83
+ return {
84
+ ok: false,
85
+ reason: "topic_spec_repo_has_no_task_queue",
86
+ index_mode: "lifecycle",
87
+ index_source: indexSource,
88
+ lifecycles,
89
+ spec_root: relOrAbs(root, indexRoot),
90
+ spec_root_mode: detected.mode,
91
+ index_path_column: found.pathLabel,
92
+ next_action: "use_other_task_source",
93
+ };
94
+ }
95
+ const idColumn = found.columns.id;
96
+ const titleColumn = found.columns.title;
97
+ const statusColumn = found.columns.status;
98
+ const pathColumn = found.columns.path;
99
+ if (
100
+ idColumn === undefined ||
101
+ titleColumn === undefined ||
102
+ statusColumn === undefined ||
103
+ pathColumn === undefined
104
+ ) {
105
+ return {
106
+ ok: false,
107
+ reason: "index_table_not_found",
108
+ path: primaryReadme,
109
+ spec_root_mode: detected.mode,
110
+ };
111
+ }
112
+ const statuses: string[] = [];
113
+ const candidates: Candidate[] = [];
114
+ const unresolved: Json[] = [];
115
+ for (const line of lines.slice(found.start)) {
116
+ if (!line.trimStart().startsWith("|")) break;
117
+ const cells = row(line);
118
+ if (!cells.length || separator(cells)) continue;
119
+ const max = Math.max(...Object.values(found.columns));
120
+ if (cells.length <= max) continue;
121
+ const id = cells[idColumn] ?? "";
122
+ const title = cells[titleColumn] ?? "";
123
+ const status = cells[statusColumn] ?? "";
124
+ if (status) statuses.push(status);
125
+ if (status !== STATUS_READY && !status.startsWith(STATUS_PARTIAL_PREFIX))
126
+ continue;
127
+ const resolved = resolveCandidate(
128
+ root,
129
+ id,
130
+ cells[pathColumn] ?? "",
131
+ detected.primary,
132
+ detected.fallback,
133
+ );
134
+ if (!resolved.path) {
135
+ unresolved.push({
136
+ spec_id: id,
137
+ plan_id: id,
138
+ title,
139
+ status,
140
+ candidates: resolved.candidates,
141
+ });
142
+ continue;
143
+ }
144
+ candidates.push({
145
+ id,
146
+ title,
147
+ status,
148
+ path: resolved.path,
149
+ createdAt: createdAt(root, resolved.path),
150
+ rank: statusRank(status),
151
+ });
152
+ }
153
+ if (unresolved.length)
154
+ return {
155
+ ok: false,
156
+ reason: "spec_path_not_unique_or_missing",
157
+ implementable_candidates: unresolved,
158
+ spec_root: relOrAbs(root, detected.primary),
159
+ spec_root_mode: detected.mode,
160
+ index_path_column: found.pathLabel,
161
+ index_mode: found.mode,
162
+ };
163
+ if (!candidates.length)
164
+ return {
165
+ ok: false,
166
+ reason: noPick(statuses),
167
+ statuses,
168
+ spec_root: relOrAbs(root, indexRoot),
169
+ spec_root_mode: detected.mode,
170
+ index_mode: found.mode,
171
+ index_source: indexSource,
172
+ };
173
+ candidates.sort(
174
+ (left, right) =>
175
+ left.rank - right.rank ||
176
+ (left.createdAt === null ? 1 : 0) - (right.createdAt === null ? 1 : 0) ||
177
+ (left.createdAt ?? "").localeCompare(right.createdAt ?? "") ||
178
+ left.id.localeCompare(right.id),
179
+ );
180
+ const pick = candidates[0] as Candidate;
181
+ return {
182
+ ok: true,
183
+ spec_id: pick.id,
184
+ plan_id: pick.id,
185
+ title: pick.title,
186
+ status: pick.status,
187
+ spec_path: pick.path,
188
+ plan_path: pick.path,
189
+ created_at: pick.createdAt,
190
+ selection_mode: "status_then_git_created_at",
191
+ spec_root: relOrAbs(root, indexRoot),
192
+ spec_root_mode: detected.mode,
193
+ index_path_column: found.pathLabel,
194
+ index_mode: found.mode,
195
+ index_source: indexSource,
196
+ next_action: "implement",
197
+ next: `implement ${pick.path}`,
198
+ };
199
+ }