@tea-agent/loop-agent 0.25.6 → 0.26.1

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 (41) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +27 -1
  3. package/README.md +8 -3
  4. package/dist/cli/command-definitions.js +25 -10
  5. package/dist/cli/help.js +4 -3
  6. package/dist/cli/program.js +43 -17
  7. package/dist/commands/import-prd.js +7 -2
  8. package/dist/commands/init.js +7 -5
  9. package/dist/commands/task-source-prepare.js +468 -0
  10. package/dist/executors/dag-pi-executor.js +66 -25
  11. package/dist/executors/model-routing.js +34 -18
  12. package/dist/executors/shell-write-guard.js +161 -25
  13. package/dist/governance/manifest-types.js +33 -5
  14. package/dist/task/source-prepare/build-draft.js +215 -0
  15. package/dist/task/source-prepare/completeness.js +195 -0
  16. package/dist/task/source-prepare/index.js +7 -0
  17. package/dist/task/source-prepare/parse-intent.js +373 -0
  18. package/dist/task/source-prepare/path-policy.js +197 -0
  19. package/dist/task/source-prepare/prepare.js +506 -0
  20. package/dist/task/source-prepare/reference-integrity.js +274 -0
  21. package/dist/task/source-prepare/types.js +7 -0
  22. package/dist/task/task-demand-routing.js +3 -1
  23. package/dist/worker/console/chat/model-resolver.js +15 -3
  24. package/dist/worker/observe/static/constants.js +3 -2
  25. package/dist/worker/observe/static/dag-model.js +1 -0
  26. package/dist/worker/observe/static/styles.css +182 -42
  27. package/dist/workflows/dag/lifecycle.js +40 -30
  28. package/dist/workflows/dag/node-execution.js +13 -0
  29. package/dist/workflows/dag/types.js +59 -19
  30. package/docs/templates/harness.schema.json +29 -7
  31. package/docs/templates/init-managed-agents.md +10 -5
  32. package/harness.json +1 -2
  33. package/package.json +1 -1
  34. package/skills/loop-agent/SKILL.md +5 -2
  35. package/skills/loop-agent/references/command-reference.md +17 -15
  36. package/skills/loop-agent/references/harness-policy.md +3 -4
  37. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  38. package/skills/loop-agent/references/model-routing.md +2 -0
  39. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  40. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  41. package/skills/loop-agent/references/task-workflow.md +7 -5
@@ -0,0 +1,195 @@
1
+ const FRESH_SOURCE_MISSING_CODES = new Set([
2
+ "MISSING_REQUIREMENT",
3
+ "MISSING_CONSTRAINTS",
4
+ ]);
5
+ export function isFreshLegacyUnversioned(state, sourceFilesPresent) {
6
+ if (state.effectiveStatus !== "legacy-unversioned")
7
+ return false;
8
+ if (state.observationErrors.some((e) => e.code === "NOT_FOUND"))
9
+ return false;
10
+ // Fresh new-task: both source files missing/empty. Any non-empty source is dirty.
11
+ if (sourceFilesPresent &&
12
+ (sourceFilesPresent.requirement || sourceFilesPresent.constraints)) {
13
+ return false;
14
+ }
15
+ const codes = state.observationErrors.map((e) => e.code);
16
+ // Allow empty errors only when caller already proved both files are empty.
17
+ if (codes.length === 0) {
18
+ return Boolean(sourceFilesPresent);
19
+ }
20
+ return codes.every((code) => FRESH_SOURCE_MISSING_CODES.has(code));
21
+ }
22
+ export function observationBlocksPrepare(state) {
23
+ if (state.observationErrors.some((e) => e.code === "NOT_FOUND")) {
24
+ return {
25
+ code: "TASK_NOT_FOUND",
26
+ level: "blocking",
27
+ message: "task not found",
28
+ };
29
+ }
30
+ if (state.effectiveStatus === "transaction-incomplete") {
31
+ return {
32
+ code: "TRANSACTION_INCOMPLETE",
33
+ level: "blocking",
34
+ message: "unfinished contract transaction; run task contract recover before prepare",
35
+ };
36
+ }
37
+ const disallowed = state.observationErrors.filter((e) => !FRESH_SOURCE_MISSING_CODES.has(e.code));
38
+ // EXTERNALLY_MODIFIED alone is the dirty-source signal handled by C6.
39
+ // Any other observation code (e.g. MANIFEST_MISSING, OBSERVE_FAILED,
40
+ // INVALID_CONTRACT_JSON) is independently blocking; force cannot bypass it.
41
+ const hard = disallowed.filter((e) => e.code !== "EXTERNALLY_MODIFIED");
42
+ if (hard.length > 0) {
43
+ return {
44
+ code: "OBSERVATION_ERROR",
45
+ level: "blocking",
46
+ message: hard.map((e) => e.message).join("; "),
47
+ };
48
+ }
49
+ return null;
50
+ }
51
+ export function listPrepareGaps(input) {
52
+ const gaps = [];
53
+ const risks = [];
54
+ const { draft } = input;
55
+ const obs = observationBlocksPrepare(input.state);
56
+ if (obs)
57
+ gaps.push(obs);
58
+ if (!draft.requirement.objective?.trim()) {
59
+ gaps.push({
60
+ code: "EMPTY_OBJECTIVE",
61
+ level: "blocking",
62
+ field: "objective",
63
+ message: "requirement.objective is required",
64
+ });
65
+ }
66
+ if (draft.requirement.acceptanceCriteria.length === 0) {
67
+ gaps.push({
68
+ code: "EMPTY_ACCEPTANCE",
69
+ level: "blocking",
70
+ field: "acceptanceCriteria",
71
+ message: "at least one acceptance criterion is required",
72
+ });
73
+ }
74
+ if (input.acceptanceConflicts && input.acceptanceConflicts.length > 0) {
75
+ for (const conflict of input.acceptanceConflicts) {
76
+ gaps.push({
77
+ code: "ACCEPTANCE_ID_CONFLICT",
78
+ level: "blocking",
79
+ field: "acceptanceCriteria",
80
+ message: `acceptance criterion id ${conflict.id} has conflicting texts`,
81
+ });
82
+ }
83
+ }
84
+ if (draft.constraints.allowedPaths.length === 0) {
85
+ gaps.push({
86
+ code: "EMPTY_ALLOWED_PATHS",
87
+ level: "blocking",
88
+ field: "allowedPaths",
89
+ message: "allowedPaths must be non-empty before apply",
90
+ });
91
+ }
92
+ if (!draft.taskKind?.trim()) {
93
+ gaps.push({
94
+ code: "EMPTY_TASK_KIND",
95
+ level: "blocking",
96
+ field: "taskKind",
97
+ message: "taskKind is required",
98
+ });
99
+ }
100
+ if (draft.taskKind === "knowledge-sync" && !draft.featureId) {
101
+ gaps.push({
102
+ code: "MISSING_FEATURE_ID",
103
+ level: "blocking",
104
+ field: "featureId",
105
+ message: "featureId is required when taskKind is knowledge-sync",
106
+ });
107
+ }
108
+ if (draft.requirement.scope.length === 0) {
109
+ gaps.push({
110
+ code: "EMPTY_SCOPE",
111
+ level: "warning",
112
+ field: "scope",
113
+ message: "scope is empty; projection will show (none)",
114
+ });
115
+ }
116
+ if (draft.requirement.nonGoals.length === 0) {
117
+ gaps.push({
118
+ code: "EMPTY_NON_GOALS",
119
+ level: "warning",
120
+ field: "nonGoals",
121
+ message: "nonGoals is empty; projection will show (none)",
122
+ });
123
+ }
124
+ if (draft.verification.commands.length === 0) {
125
+ gaps.push({
126
+ code: "EMPTY_VERIFY",
127
+ level: "warning",
128
+ field: "verifyCommands",
129
+ message: "verifyCommands is empty",
130
+ });
131
+ }
132
+ if (input.sourceIntegrity) {
133
+ for (const issue of input.sourceIntegrity.issues) {
134
+ if (issue.level === "blocking") {
135
+ gaps.push({
136
+ code: issue.code,
137
+ level: "blocking",
138
+ message: issue.message,
139
+ field: issue.path,
140
+ });
141
+ }
142
+ else {
143
+ gaps.push({
144
+ code: issue.code,
145
+ level: "warning",
146
+ message: issue.message,
147
+ field: issue.path,
148
+ });
149
+ }
150
+ }
151
+ }
152
+ if (input.pathAssessment) {
153
+ risks.push(...input.pathAssessment.blocking, ...input.pathAssessment.elevated, ...input.pathAssessment.warnings);
154
+ for (const risk of input.pathAssessment.blocking) {
155
+ gaps.push({
156
+ code: risk.code,
157
+ level: "blocking",
158
+ message: risk.message,
159
+ field: risk.path,
160
+ });
161
+ }
162
+ }
163
+ // Dirty source gate (C6 / D14)
164
+ const status = input.state.effectiveStatus;
165
+ const hasExistingSource = Boolean(input.sourceFilesPresent?.requirement ||
166
+ input.sourceFilesPresent?.constraints);
167
+ const hasDirtySource = status === "externally-modified" ||
168
+ (status === "legacy-unversioned" && hasExistingSource);
169
+ if (hasDirtySource && !input.forceOverwriteSource) {
170
+ gaps.push({
171
+ code: "DIRTY_SOURCE",
172
+ level: "blocking",
173
+ message: "existing non-empty or externally-modified source refuses --apply; use show/diff/adopt/recover or --force-overwrite-source",
174
+ });
175
+ risks.push({
176
+ code: "DIRTY_SOURCE",
177
+ level: "blocking",
178
+ message: "source is dirty/externally-modified; prepare will not overwrite without --force-overwrite-source",
179
+ });
180
+ }
181
+ if (!input.hasImportedPrd) {
182
+ gaps.push({
183
+ code: "NO_IMPORTED_PRD",
184
+ level: "warning",
185
+ message: "no imported PRD in use; prefer detailed PRD → import-prd → prepare (escape hatch only)",
186
+ });
187
+ }
188
+ return { gaps, risks };
189
+ }
190
+ export function hasBlockingGaps(gaps) {
191
+ return gaps.some((gap) => gap.level === "blocking");
192
+ }
193
+ export function hasBlockingRisks(risks) {
194
+ return risks.some((risk) => risk.level === "blocking");
195
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./types.js";
2
+ export * from "./path-policy.js";
3
+ export * from "./parse-intent.js";
4
+ export * from "./reference-integrity.js";
5
+ export * from "./completeness.js";
6
+ export * from "./build-draft.js";
7
+ export * from "./prepare.js";
@@ -0,0 +1,373 @@
1
+ const HEADING_ALIASES = {
2
+ objective: new Set(["目标", "objective", "问题与目标"]),
3
+ scope: new Set(["范围", "scope", "in scope"]),
4
+ nonGoals: new Set(["非目标", "out of scope", "non-goals", "non goals"]),
5
+ acceptance: new Set(["验收标准", "acceptance criteria", "acceptance"]),
6
+ constraints: new Set(["约束", "constraints", "不变式", "invariants"]),
7
+ openQuestions: new Set(["待决问题", "open questions", "open-questions"]),
8
+ assumptions: new Set(["假设", "assumptions"]),
9
+ };
10
+ const OBJECTIVE_MAX_CHARS = 1200;
11
+ function stripFencedAndComments(text) {
12
+ // Remove fenced code blocks and HTML comments so headings inside them are ignored.
13
+ return text
14
+ .replace(/```[\s\S]*?```/g, "\n")
15
+ .replace(/~~~[\s\S]*?~~~/g, "\n")
16
+ .replace(/<!--[\s\S]*?-->/g, "\n");
17
+ }
18
+ function normalizeHeadingText(raw) {
19
+ return raw
20
+ .trim()
21
+ .toLowerCase()
22
+ .replace(/[::]\s*$/u, "")
23
+ .replace(/\s+/g, " ");
24
+ }
25
+ function classifyHeading(text) {
26
+ const normalized = normalizeHeadingText(text);
27
+ for (const [key, aliases] of Object.entries(HEADING_ALIASES)) {
28
+ if (aliases.has(normalized))
29
+ return key;
30
+ }
31
+ return "other";
32
+ }
33
+ function isAtxHeading(line) {
34
+ const match = /^(#{1,6})\s+(.+?)\s*$/u.exec(line);
35
+ if (!match)
36
+ return null;
37
+ return { level: match[1].length, text: match[2] };
38
+ }
39
+ function parseListItem(line) {
40
+ const match = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[(?: |x|X)\]\s+)?(.+?)\s*$/u.exec(line);
41
+ if (!match)
42
+ return null;
43
+ return match[1].trim();
44
+ }
45
+ function parseAcceptanceLine(line, nextId) {
46
+ const checklist = /^\s*[-*+]\s+\[(?: |x|X)\]\s+(.+?)\s*$/u.exec(line);
47
+ const bullet = /^\s*(?:[-*+]|\d+[.)])\s+(.+?)\s*$/u.exec(line);
48
+ const body = (checklist?.[1] ?? bullet?.[1] ?? line.trim()).trim();
49
+ if (!body)
50
+ return null;
51
+ const withId = /^(AC[-_]?\d+)\s*[::]\s*(.+)$/iu.exec(body);
52
+ if (withId) {
53
+ return { id: withId[1].toUpperCase().replace("_", "-"), text: withId[2].trim() };
54
+ }
55
+ const bareAc = /^(AC[-_]?\d+)\s+(.+)$/iu.exec(body);
56
+ if (bareAc) {
57
+ return {
58
+ id: bareAc[1].toUpperCase().replace("_", "-"),
59
+ text: bareAc[2].trim(),
60
+ };
61
+ }
62
+ return { id: nextId(), text: body };
63
+ }
64
+ function firstParagraph(lines) {
65
+ const chunks = [];
66
+ let collecting = false;
67
+ for (const line of lines) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed) {
70
+ if (collecting)
71
+ break;
72
+ continue;
73
+ }
74
+ if (isAtxHeading(trimmed))
75
+ break;
76
+ if (parseListItem(trimmed)) {
77
+ if (!collecting) {
78
+ // Prefer list items only if no paragraph started.
79
+ return parseListItem(trimmed) ?? trimmed;
80
+ }
81
+ break;
82
+ }
83
+ collecting = true;
84
+ chunks.push(trimmed);
85
+ }
86
+ return chunks.join(" ").trim();
87
+ }
88
+ function collectListItems(lines) {
89
+ const items = [];
90
+ for (const line of lines) {
91
+ const item = parseListItem(line);
92
+ if (item)
93
+ items.push(item);
94
+ }
95
+ return items;
96
+ }
97
+ function extractTitle(markdown) {
98
+ const lines = stripFencedAndComments(markdown).split(/\r?\n/);
99
+ for (const line of lines) {
100
+ const heading = isAtxHeading(line);
101
+ if (heading && heading.level === 1) {
102
+ const text = heading.text.trim();
103
+ return text || undefined;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+ /**
109
+ * Deterministic heading-based extraction. No LLM. No engineering-field extraction.
110
+ */
111
+ export function extractRequirementFactsFromMarkdown(markdown, options) {
112
+ const cleaned = stripFencedAndComments(markdown);
113
+ const lines = cleaned.split(/\r?\n/);
114
+ const sections = [];
115
+ let current = { key: "other", lines: [] };
116
+ sections.push(current);
117
+ for (const line of lines) {
118
+ const heading = isAtxHeading(line);
119
+ if (heading) {
120
+ const key = classifyHeading(heading.text);
121
+ current = { key, lines: [] };
122
+ sections.push(current);
123
+ continue;
124
+ }
125
+ current.lines.push(line);
126
+ }
127
+ let acCounter = options?.acCounterStart ?? 1;
128
+ const nextId = () => {
129
+ const id = `AC-${String(acCounter).padStart(3, "0")}`;
130
+ acCounter += 1;
131
+ return id;
132
+ };
133
+ let objective;
134
+ const scope = [];
135
+ const nonGoals = [];
136
+ const acceptanceCriteria = [];
137
+ const invariants = [];
138
+ const openQuestions = [];
139
+ const assumptions = [];
140
+ for (const section of sections) {
141
+ if (section.key === "objective" && !objective) {
142
+ const paragraph = firstParagraph(section.lines);
143
+ if (paragraph) {
144
+ objective =
145
+ paragraph.length > OBJECTIVE_MAX_CHARS
146
+ ? paragraph.slice(0, OBJECTIVE_MAX_CHARS)
147
+ : paragraph;
148
+ }
149
+ }
150
+ else if (section.key === "scope") {
151
+ scope.push(...collectListItems(section.lines));
152
+ }
153
+ else if (section.key === "nonGoals") {
154
+ nonGoals.push(...collectListItems(section.lines));
155
+ }
156
+ else if (section.key === "acceptance") {
157
+ for (const line of section.lines) {
158
+ const trimmed = line.trim();
159
+ if (!trimmed)
160
+ continue;
161
+ // Only list/checklist lines become AC; bare paragraphs ignored for stability.
162
+ if (!/^\s*(?:[-*+]|\d+[.)])\s+/.test(line))
163
+ continue;
164
+ const ac = parseAcceptanceLine(line, nextId);
165
+ if (ac?.text)
166
+ acceptanceCriteria.push(ac);
167
+ }
168
+ }
169
+ else if (section.key === "constraints") {
170
+ invariants.push(...collectListItems(section.lines));
171
+ }
172
+ else if (section.key === "openQuestions") {
173
+ openQuestions.push(...collectListItems(section.lines));
174
+ }
175
+ else if (section.key === "assumptions") {
176
+ assumptions.push(...collectListItems(section.lines));
177
+ }
178
+ }
179
+ // Fallback: first non-heading paragraph as objective when no objective section.
180
+ if (!objective) {
181
+ const bodyLines = lines.filter((line) => !isAtxHeading(line));
182
+ const paragraph = firstParagraph(bodyLines);
183
+ if (paragraph) {
184
+ objective =
185
+ paragraph.length > OBJECTIVE_MAX_CHARS
186
+ ? paragraph.slice(0, OBJECTIVE_MAX_CHARS)
187
+ : paragraph;
188
+ }
189
+ }
190
+ const acceptance = dedupeAcceptance(acceptanceCriteria);
191
+ return {
192
+ title: extractTitle(markdown),
193
+ objective,
194
+ scope: dedupeText(scope),
195
+ nonGoals: dedupeText(nonGoals),
196
+ acceptanceCriteria: acceptance.items,
197
+ invariants: dedupeText(invariants),
198
+ openQuestions: dedupeText(openQuestions),
199
+ assumptions: dedupeText(assumptions),
200
+ sourceLabels: options?.sourceLabel ? [options.sourceLabel] : [],
201
+ acceptanceConflicts: acceptance.conflicts,
202
+ };
203
+ }
204
+ function dedupeText(items) {
205
+ const seen = new Set();
206
+ const out = [];
207
+ for (const item of items) {
208
+ const trimmed = item.trim();
209
+ if (!trimmed || seen.has(trimmed))
210
+ continue;
211
+ seen.add(trimmed);
212
+ out.push(trimmed);
213
+ }
214
+ return out;
215
+ }
216
+ function dedupeAcceptance(items) {
217
+ const byId = new Map();
218
+ const order = [];
219
+ const conflicts = new Map();
220
+ for (const item of items) {
221
+ const id = item.id.trim();
222
+ const text = item.text.trim();
223
+ if (!id || !text)
224
+ continue;
225
+ const existing = byId.get(id);
226
+ if (existing === undefined) {
227
+ byId.set(id, text);
228
+ order.push(id);
229
+ continue;
230
+ }
231
+ if (existing !== text) {
232
+ const texts = conflicts.get(id) ?? new Set([existing]);
233
+ texts.add(text);
234
+ conflicts.set(id, texts);
235
+ }
236
+ }
237
+ return {
238
+ items: order.map((id) => ({ id, text: byId.get(id) })),
239
+ conflicts: [...conflicts].map(([id, texts]) => ({ id, texts: [...texts] })),
240
+ };
241
+ }
242
+ export function mergeAcceptanceCriteria(parts) {
243
+ const byId = new Map();
244
+ const order = [];
245
+ for (const group of parts) {
246
+ for (const item of group) {
247
+ const id = item.id.trim();
248
+ const text = item.text.trim();
249
+ if (!id || !text)
250
+ continue;
251
+ if (!byId.has(id)) {
252
+ byId.set(id, new Set());
253
+ order.push(id);
254
+ }
255
+ byId.get(id)?.add(text);
256
+ }
257
+ }
258
+ const conflicts = [];
259
+ const items = [];
260
+ for (const id of order) {
261
+ const texts = [...(byId.get(id) ?? [])];
262
+ if (texts.length === 1) {
263
+ items.push({ id, text: texts[0] });
264
+ }
265
+ else if (texts.length > 1) {
266
+ conflicts.push({ id, texts });
267
+ }
268
+ }
269
+ return { items, conflicts };
270
+ }
271
+ export function mergeRequirementFacts(parts) {
272
+ let title;
273
+ let objective;
274
+ const scope = [];
275
+ const nonGoals = [];
276
+ const invariants = [];
277
+ const openQuestions = [];
278
+ const assumptions = [];
279
+ const sourceLabels = [];
280
+ const acGroups = [];
281
+ const internalAcceptanceConflicts = [];
282
+ for (const part of parts) {
283
+ if (!title && part.title)
284
+ title = part.title;
285
+ if (!objective && part.objective)
286
+ objective = part.objective;
287
+ scope.push(...part.scope);
288
+ nonGoals.push(...part.nonGoals);
289
+ invariants.push(...part.invariants);
290
+ openQuestions.push(...part.openQuestions);
291
+ assumptions.push(...part.assumptions);
292
+ sourceLabels.push(...part.sourceLabels);
293
+ acGroups.push(part.acceptanceCriteria);
294
+ internalAcceptanceConflicts.push(...(part.acceptanceConflicts ?? []));
295
+ }
296
+ const ac = mergeAcceptanceCriteria(acGroups);
297
+ return {
298
+ title,
299
+ objective,
300
+ scope: dedupeText(scope),
301
+ nonGoals: dedupeText(nonGoals),
302
+ acceptanceCriteria: ac.items,
303
+ invariants: dedupeText(invariants),
304
+ openQuestions: dedupeText(openQuestions),
305
+ assumptions: dedupeText(assumptions),
306
+ sourceLabels: dedupeText(sourceLabels),
307
+ acceptanceConflicts: [
308
+ ...internalAcceptanceConflicts,
309
+ ...ac.conflicts,
310
+ ],
311
+ };
312
+ }
313
+ /**
314
+ * Supplemental text only fills missing PRD fields (C3.1).
315
+ * Does not append/override scope, nonGoals, or AC when base already has values.
316
+ */
317
+ export function fillMissingRequirementFacts(base, supplemental) {
318
+ const acceptance = base.acceptanceCriteria.length > 0
319
+ ? base.acceptanceCriteria
320
+ : supplemental.acceptanceCriteria;
321
+ return {
322
+ title: base.title ?? supplemental.title,
323
+ objective: base.objective ?? supplemental.objective,
324
+ scope: base.scope.length > 0 ? base.scope : supplemental.scope,
325
+ nonGoals: base.nonGoals.length > 0 ? base.nonGoals : supplemental.nonGoals,
326
+ acceptanceCriteria: acceptance,
327
+ invariants: base.invariants.length > 0 ? base.invariants : supplemental.invariants,
328
+ openQuestions: base.openQuestions.length > 0
329
+ ? base.openQuestions
330
+ : supplemental.openQuestions,
331
+ assumptions: base.assumptions.length > 0
332
+ ? base.assumptions
333
+ : supplemental.assumptions,
334
+ sourceLabels: dedupeText([
335
+ ...base.sourceLabels,
336
+ ...supplemental.sourceLabels,
337
+ ]),
338
+ acceptanceConflicts: base.acceptanceConflicts ?? [],
339
+ };
340
+ }
341
+ export function parseVerifyFlag(raw) {
342
+ const idx = raw.indexOf(":");
343
+ if (idx <= 0 || idx === raw.length - 1) {
344
+ throw new Error(`invalid --verify value "${raw}"; expected <label>:<command>`);
345
+ }
346
+ return {
347
+ label: raw.slice(0, idx).trim(),
348
+ command: raw.slice(idx + 1).trim(),
349
+ };
350
+ }
351
+ export function parseVerifyTimeoutFlag(raw) {
352
+ const idx = raw.indexOf(":");
353
+ if (idx <= 0 || idx === raw.length - 1) {
354
+ throw new Error(`invalid --verify-timeout value "${raw}"; expected <label>:<timeoutMs>`);
355
+ }
356
+ const label = raw.slice(0, idx).trim();
357
+ const timeoutRaw = raw.slice(idx + 1).trim();
358
+ const timeoutMs = Number(timeoutRaw);
359
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
360
+ throw new Error(`invalid --verify-timeout value "${raw}"; timeoutMs must be a positive integer`);
361
+ }
362
+ return { label, timeoutMs };
363
+ }
364
+ export function parseAcceptanceFlag(raw) {
365
+ const idx = raw.indexOf(":");
366
+ if (idx <= 0 || idx === raw.length - 1) {
367
+ throw new Error(`invalid --ac/--acceptance value "${raw}"; expected <id>:<text>`);
368
+ }
369
+ return {
370
+ id: raw.slice(0, idx).trim(),
371
+ text: raw.slice(idx + 1).trim(),
372
+ };
373
+ }