@tea-agent/loop-agent 0.26.0 → 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.
- package/CHANGELOG.md +1032 -1020
- package/README.md +8 -3
- package/bin/loop-agent.js +21 -21
- package/dist/cli/command-definitions.js +25 -10
- package/dist/cli/help.js +4 -3
- package/dist/cli/program.js +43 -17
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/import-prd.js +7 -2
- package/dist/commands/init.js +7 -5
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/commands/task-source-prepare.js +468 -0
- package/dist/executors/dag-pi-executor.js +40 -5
- package/dist/executors/shell-write-guard.js +161 -25
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/source-prepare/build-draft.js +215 -0
- package/dist/task/source-prepare/completeness.js +195 -0
- package/dist/task/source-prepare/index.js +7 -0
- package/dist/task/source-prepare/parse-intent.js +373 -0
- package/dist/task/source-prepare/path-policy.js +197 -0
- package/dist/task/source-prepare/prepare.js +506 -0
- package/dist/task/source-prepare/reference-integrity.js +274 -0
- package/dist/task/source-prepare/types.js +7 -0
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/docs/skills/README.md +7 -7
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +10 -5
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/SKILL.md +5 -2
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +17 -15
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/harness-policy.md +3 -4
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
- package/skills/loop-agent/references/task-workflow.md +7 -5
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { canonicalizePath } from "../contract/canonicalize.js";
|
|
2
|
+
import { PREPARE_DEFAULT_FORBIDDEN_PATHS } from "./types.js";
|
|
3
|
+
function hasNul(value) {
|
|
4
|
+
return value.includes("\0");
|
|
5
|
+
}
|
|
6
|
+
function isAbsolutePath(value) {
|
|
7
|
+
if (value.startsWith("/"))
|
|
8
|
+
return true;
|
|
9
|
+
// Windows drive or UNC
|
|
10
|
+
if (/^[a-zA-Z]:[\\/]/.test(value))
|
|
11
|
+
return true;
|
|
12
|
+
if (value.startsWith("\\\\") || value.startsWith("//"))
|
|
13
|
+
return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function hasTraversalSegment(value) {
|
|
17
|
+
const parts = value.split("/");
|
|
18
|
+
return parts.some((part) => part === "..");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalize path globs for policy comparison:
|
|
22
|
+
* - trim
|
|
23
|
+
* - backslash → /
|
|
24
|
+
* - collapse leading ./
|
|
25
|
+
* - reject empty / NUL
|
|
26
|
+
*/
|
|
27
|
+
export function normalizePreparePath(raw) {
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
return { ok: false, reason: "path is empty" };
|
|
31
|
+
}
|
|
32
|
+
if (hasNul(trimmed)) {
|
|
33
|
+
return { ok: false, reason: "path contains NUL" };
|
|
34
|
+
}
|
|
35
|
+
let normalized = canonicalizePath(trimmed);
|
|
36
|
+
while (normalized.startsWith("./")) {
|
|
37
|
+
normalized = normalized.slice(2);
|
|
38
|
+
}
|
|
39
|
+
if (normalized === "./" || normalized === ".") {
|
|
40
|
+
normalized = ".";
|
|
41
|
+
}
|
|
42
|
+
if (normalized === "./**") {
|
|
43
|
+
normalized = "**";
|
|
44
|
+
}
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
return { ok: false, reason: "path is empty after normalization" };
|
|
47
|
+
}
|
|
48
|
+
if (isAbsolutePath(normalized)) {
|
|
49
|
+
return { ok: false, reason: "absolute paths are not allowed" };
|
|
50
|
+
}
|
|
51
|
+
if (hasTraversalSegment(normalized)) {
|
|
52
|
+
return { ok: false, reason: "path traversal (..) is not allowed" };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, value: normalized };
|
|
55
|
+
}
|
|
56
|
+
export function dedupeStable(paths) {
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const item of paths) {
|
|
60
|
+
if (seen.has(item))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(item);
|
|
63
|
+
out.push(item);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Protected defaults always merge unless noDefaultForbiddenPaths is set.
|
|
69
|
+
* Explicit forbiddenPaths append.
|
|
70
|
+
*/
|
|
71
|
+
export function mergeForbiddenPaths(input) {
|
|
72
|
+
const parts = [];
|
|
73
|
+
if (!input.noDefaultForbiddenPaths) {
|
|
74
|
+
parts.push(...PREPARE_DEFAULT_FORBIDDEN_PATHS);
|
|
75
|
+
}
|
|
76
|
+
if (input.existing)
|
|
77
|
+
parts.push(...input.existing);
|
|
78
|
+
if (input.explicit)
|
|
79
|
+
parts.push(...input.explicit);
|
|
80
|
+
const normalized = [];
|
|
81
|
+
for (const raw of parts) {
|
|
82
|
+
const result = normalizePreparePath(raw);
|
|
83
|
+
if (result.ok)
|
|
84
|
+
normalized.push(result.value);
|
|
85
|
+
else
|
|
86
|
+
normalized.push(raw.trim());
|
|
87
|
+
}
|
|
88
|
+
return dedupeStable(normalized);
|
|
89
|
+
}
|
|
90
|
+
function forbiddenAncestorCoversAllowed(allowed, forbidden) {
|
|
91
|
+
// Only the deterministic prefix/** ancestor rule from C2.1.
|
|
92
|
+
if (!forbidden.endsWith("/**"))
|
|
93
|
+
return false;
|
|
94
|
+
const prefix = forbidden.slice(0, -3); // drop /**
|
|
95
|
+
if (!prefix)
|
|
96
|
+
return false;
|
|
97
|
+
if (allowed === prefix)
|
|
98
|
+
return true;
|
|
99
|
+
if (allowed.startsWith(`${prefix}/`))
|
|
100
|
+
return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
export function assessPreparePaths(input) {
|
|
104
|
+
const blocking = [];
|
|
105
|
+
const elevated = [];
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const allowed = [];
|
|
108
|
+
const forbidden = [];
|
|
109
|
+
for (const raw of input.allowedPaths) {
|
|
110
|
+
const result = normalizePreparePath(raw);
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
blocking.push({
|
|
113
|
+
code: "UNSAFE_ALLOWED_PATH",
|
|
114
|
+
level: "blocking",
|
|
115
|
+
message: `allowed path invalid: ${raw} (${result.reason})`,
|
|
116
|
+
path: raw,
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
allowed.push(result.value);
|
|
121
|
+
}
|
|
122
|
+
for (const raw of input.forbiddenPaths) {
|
|
123
|
+
const result = normalizePreparePath(raw);
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
blocking.push({
|
|
126
|
+
code: "UNSAFE_FORBIDDEN_PATH",
|
|
127
|
+
level: "blocking",
|
|
128
|
+
message: `forbidden path invalid: ${raw} (${result.reason})`,
|
|
129
|
+
path: raw,
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
forbidden.push(result.value);
|
|
134
|
+
}
|
|
135
|
+
const allowedSet = new Set(allowed);
|
|
136
|
+
for (const f of forbidden) {
|
|
137
|
+
if (allowedSet.has(f)) {
|
|
138
|
+
blocking.push({
|
|
139
|
+
code: "ALLOWED_FORBIDDEN_OVERLAP",
|
|
140
|
+
level: "blocking",
|
|
141
|
+
message: `allowedPaths and forbiddenPaths overlap exactly: ${f}`,
|
|
142
|
+
path: f,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const a of allowed) {
|
|
147
|
+
for (const f of forbidden) {
|
|
148
|
+
if (forbiddenAncestorCoversAllowed(a, f)) {
|
|
149
|
+
blocking.push({
|
|
150
|
+
code: "ALLOWED_UNDER_FORBIDDEN_ANCESTOR",
|
|
151
|
+
level: "blocking",
|
|
152
|
+
message: `allowed path ${a} is fully covered by forbidden ancestor ${f}`,
|
|
153
|
+
path: a,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const a of allowed) {
|
|
159
|
+
if (a === "**" || a === "*" || a === ".") {
|
|
160
|
+
elevated.push({
|
|
161
|
+
code: "BROAD_ALLOWED_ROOT",
|
|
162
|
+
level: "elevated",
|
|
163
|
+
message: `allowed path ${a} grants repo-root broad write`,
|
|
164
|
+
path: a,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (a === "src/**") {
|
|
168
|
+
elevated.push({
|
|
169
|
+
code: "BROAD_ALLOWED_SRC",
|
|
170
|
+
level: "elevated",
|
|
171
|
+
message: "allowed path src/** is broad for large repos; prefer a narrower glob",
|
|
172
|
+
path: a,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (input.noDefaultForbiddenPaths) {
|
|
177
|
+
elevated.push({
|
|
178
|
+
code: "DEFAULT_FORBIDDEN_DISABLED",
|
|
179
|
+
level: "elevated",
|
|
180
|
+
message: "--no-default-forbidden-paths disabled protected defaults (.harness/**, node_modules/**)",
|
|
181
|
+
});
|
|
182
|
+
if (!forbidden.includes(".harness/**")) {
|
|
183
|
+
warnings.push({
|
|
184
|
+
code: "MISSING_HARNESS_FORBIDDEN",
|
|
185
|
+
level: "routine",
|
|
186
|
+
message: "forbiddenPaths lacks .harness/** after disabling protected defaults",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
allowed: dedupeStable(allowed),
|
|
192
|
+
forbidden: dedupeStable(forbidden),
|
|
193
|
+
blocking,
|
|
194
|
+
elevated,
|
|
195
|
+
warnings,
|
|
196
|
+
};
|
|
197
|
+
}
|