@tea-agent/loop-agent 0.1.0
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/AGENTS.md +121 -0
- package/CHANGELOG.md +37 -0
- package/README.md +144 -0
- package/bin/loop-agent.js +21 -0
- package/dist/adapters/aimax.js +91 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/loop-agent.js +98 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/catalog.js +259 -0
- package/dist/cli/help.js +55 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.js +505 -0
- package/dist/cli.js +12 -0
- package/dist/commands/closeout.js +13 -0
- package/dist/commands/coverage-audit.js +14 -0
- package/dist/commands/cursor-prompt.js +222 -0
- package/dist/commands/cursor-worker.js +43 -0
- package/dist/commands/dag-approve.js +102 -0
- package/dist/commands/dag-final-verification.js +76 -0
- package/dist/commands/dag-init-hybrid.js +56 -0
- package/dist/commands/dag-reconcile-tasks.js +51 -0
- package/dist/commands/dag-reject.js +91 -0
- package/dist/commands/dag-report.js +177 -0
- package/dist/commands/dag-resume.js +34 -0
- package/dist/commands/dag-run-task.js +470 -0
- package/dist/commands/dag-validate.js +186 -0
- package/dist/commands/dag-workflow-compile.js +91 -0
- package/dist/commands/dag-workflow-plan.js +130 -0
- package/dist/commands/dag-workflow-validate.js +66 -0
- package/dist/commands/delegate.js +132 -0
- package/dist/commands/docs-archive.js +5 -0
- package/dist/commands/docs-audit.js +5 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/goal.js +92 -0
- package/dist/commands/handoff-check.js +5 -0
- package/dist/commands/harvest.js +44 -0
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/instructions.js +195 -0
- package/dist/commands/knowledge.js +64 -0
- package/dist/commands/loop-benchmark.js +72 -0
- package/dist/commands/loop.js +241 -0
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/pi-prompt.js +181 -0
- package/dist/commands/pi-reuse-benchmark.js +153 -0
- package/dist/commands/plan-list.js +5 -0
- package/dist/commands/promote-run.js +29 -0
- package/dist/commands/reference-index.js +16 -0
- package/dist/commands/run-dag.js +184 -0
- package/dist/commands/spine.js +38 -0
- package/dist/commands/stats.js +84 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/study-init.js +192 -0
- package/dist/commands/workflow.js +259 -0
- package/dist/commands/worktree-create.js +31 -0
- package/dist/commands/worktree-list.js +5 -0
- package/dist/commands/worktree-remove.js +26 -0
- package/dist/cursor-worker-entry.js +8 -0
- package/dist/executors/config-core.js +55 -0
- package/dist/executors/config.js +2 -0
- package/dist/executors/cursor-artifacts.js +33 -0
- package/dist/executors/cursor-execution-log.js +81 -0
- package/dist/executors/cursor-executor-artifacts.js +135 -0
- package/dist/executors/cursor-executor.js +468 -0
- package/dist/executors/cursor-run.js +115 -0
- package/dist/executors/cursor-tool.js +94 -0
- package/dist/executors/cursor-worker-client.js +213 -0
- package/dist/executors/cursor-worker-protocol.js +18 -0
- package/dist/executors/cursor-worker-server.js +54 -0
- package/dist/executors/cursor-worker.js +3 -0
- package/dist/executors/cursor.js +6 -0
- package/dist/executors/dag-cursor-executor.js +88 -0
- package/dist/executors/dag-pi-executor.js +322 -0
- package/dist/executors/dag-static-executor.js +45 -0
- package/dist/executors/dag.js +4 -0
- package/dist/executors/index.js +8 -0
- package/dist/executors/model-routing.js +60 -0
- package/dist/executors/pi-event-serializer.js +43 -0
- package/dist/executors/pi-executor.js +606 -0
- package/dist/executors/pi-reuse-benchmark.js +316 -0
- package/dist/executors/pi-runtime-reuse.js +29 -0
- package/dist/executors/pi-sdk-executor.js +255 -0
- package/dist/executors/pi-sdk.js +1 -0
- package/dist/executors/pi.js +3 -0
- package/dist/executors/shell-executor.js +300 -0
- package/dist/executors/shell-presets.js +47 -0
- package/dist/executors/shell-verification.js +251 -0
- package/dist/executors/shell-write-guard.js +126 -0
- package/dist/executors/shell.js +3 -0
- package/dist/executors/static.js +1 -0
- package/dist/governance/checks.js +434 -0
- package/dist/governance/harness.js +9 -0
- package/dist/governance/index.js +3 -0
- package/dist/governance/manifest-types.js +128 -0
- package/dist/governance/manifest.js +2 -0
- package/dist/governance/path-guard.js +69 -0
- package/dist/governance/path-guards.js +2 -0
- package/dist/governance/profiles.js +3 -0
- package/dist/governance/requirement-coverage.js +425 -0
- package/dist/governance/skill-safety.js +135 -0
- package/dist/governance/spine-audit.js +152 -0
- package/dist/records/closeout.js +2 -0
- package/dist/records/harvest.js +236 -0
- package/dist/records/index.js +3 -0
- package/dist/records/one-shot-runs.js +421 -0
- package/dist/records/promotion.js +199 -0
- package/dist/shared/artifacts-core.js +88 -0
- package/dist/shared/artifacts.js +2 -0
- package/dist/shared/context-files.js +32 -0
- package/dist/shared/context.js +2 -0
- package/dist/shared/copy-dir.js +17 -0
- package/dist/shared/git-progress.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/logger.js +23 -0
- package/dist/shared/one-shot-prompt-args.js +98 -0
- package/dist/shared/path-refs.js +31 -0
- package/dist/shared/prompts.js +26 -0
- package/dist/shared/reference-context.js +238 -0
- package/dist/shared/timeout-policy.js +19 -0
- package/dist/shared/timeout.js +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/task/config-types.js +97 -0
- package/dist/task/config.js +2 -0
- package/dist/task/delegate.js +220 -0
- package/dist/task/goal-audit.js +51 -0
- package/dist/task/goal-policy.js +8 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/ids.js +1 -0
- package/dist/task/index.js +9 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/read-model.js +149 -0
- package/dist/task/runtime.js +699 -0
- package/dist/task/source-state.js +1 -0
- package/dist/task/state.js +55 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/task/workflow-state-types.js +92 -0
- package/dist/task/worktree-cleanup.js +140 -0
- package/dist/task/worktree.js +171 -0
- package/dist/workflows/dag/authoring.js +8 -0
- package/dist/workflows/dag/authority-surface.js +138 -0
- package/dist/workflows/dag/canvas-observer.js +474 -0
- package/dist/workflows/dag/decision-envelope.js +502 -0
- package/dist/workflows/dag/decision-evidence.js +153 -0
- package/dist/workflows/dag/decision-gates.js +1 -0
- package/dist/workflows/dag/executor-registry.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/failure-category.js +111 -0
- package/dist/workflows/dag/final-verification.js +180 -0
- package/dist/workflows/dag/governance-constants.js +5 -0
- package/dist/workflows/dag/governance-profile.js +405 -0
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +855 -0
- package/dist/workflows/dag/knowledge-curator.js +162 -0
- package/dist/workflows/dag/lifecycle.js +484 -0
- package/dist/workflows/dag/prompt-source.js +88 -0
- package/dist/workflows/dag/prompt.js +130 -0
- package/dist/workflows/dag/reconcile-tasks.js +404 -0
- package/dist/workflows/dag/recovery-recommendation.js +226 -0
- package/dist/workflows/dag/repair-artifact.js +136 -0
- package/dist/workflows/dag/report.js +1019 -0
- package/dist/workflows/dag/runner.js +1677 -0
- package/dist/workflows/dag/runtime.js +5 -0
- package/dist/workflows/dag/skill-instructions.js +471 -0
- package/dist/workflows/dag/skills.js +41 -0
- package/dist/workflows/dag/spec.js +3 -0
- package/dist/workflows/dag/topo.js +30 -0
- package/dist/workflows/dag/types.js +275 -0
- package/dist/workflows/dag/upstream-artifacts.js +95 -0
- package/dist/workflows/dag/validate.js +527 -0
- package/dist/workflows/dynamic/artifacts.js +65 -0
- package/dist/workflows/dynamic/compile.js +360 -0
- package/dist/workflows/dynamic/compileTypes.js +1 -0
- package/dist/workflows/dynamic/errors.js +5 -0
- package/dist/workflows/dynamic/index.js +7 -0
- package/dist/workflows/dynamic/profiles.js +156 -0
- package/dist/workflows/dynamic/spec.js +114 -0
- package/dist/workflows/dynamic/validate.js +275 -0
- package/dist/workflows/loop/actions.js +1334 -0
- package/dist/workflows/loop/benchmark.js +510 -0
- package/dist/workflows/loop/closeout.js +134 -0
- package/dist/workflows/loop/context.js +48 -0
- package/dist/workflows/loop/events.js +25 -0
- package/dist/workflows/loop/hash.js +32 -0
- package/dist/workflows/loop/index.js +8 -0
- package/dist/workflows/loop/paths.js +17 -0
- package/dist/workflows/loop/rounds.js +81 -0
- package/dist/workflows/loop/signals.js +55 -0
- package/dist/workflows/loop/state.js +116 -0
- package/dist/workflows/loop/templates.js +54 -0
- package/dist/workflows/loop/types.js +28 -0
- package/docs/README.md +62 -0
- package/docs/agent-dag-recovery-playbook.md +158 -0
- package/docs/agent-dag-runner.md +40 -0
- package/docs/cursor-executor-usage.md +25 -0
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +36 -0
- package/docs/development-principles.md +71 -0
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
- package/docs/exec-plans/README.md +6 -0
- package/docs/exec-plans/active/README.md +5 -0
- package/docs/exec-plans/completed/README.md +5 -0
- package/docs/feature-workflow.md +184 -0
- package/docs/harness-methodology-debugging.md +153 -0
- package/docs/harness-methodology-tdd.md +130 -0
- package/docs/harness-methodology-verification.md +27 -0
- package/docs/loop-agent-harness.md +42 -0
- package/docs/progress/README.md +3 -0
- package/docs/reports/README.md +3 -0
- package/docs/templates/adr.md +60 -0
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
- package/docs/templates/agent-dag-report.schema.json +423 -0
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
- package/docs/templates/agent-dag.base.json +195 -0
- package/docs/templates/agent-dag.final-verification.json +190 -0
- package/docs/templates/agent-dag.schema.json +316 -0
- package/docs/templates/agent-dag.supervised-implementation.json +500 -0
- package/docs/templates/exec-plan.md +64 -0
- package/docs/templates/feature-spec.md +53 -0
- package/docs/templates/hybrid-dag.json +193 -0
- package/docs/templates/progress-log.md +17 -0
- package/docs/templates/project-start-checklist.md +9 -0
- package/docs/templates/qa-report.md +42 -0
- package/docs/templates/sprint-contract.md +29 -0
- package/docs/verification-matrix.md +30 -0
- package/examples/decision-gate-agent-dag.json +123 -0
- package/examples/example-dag.json +51 -0
- package/examples/hybrid-loop-agent-dag.json +194 -0
- package/harness.json +92 -0
- package/package.json +61 -0
- package/skills/ai-engineering-context/SKILL.md +48 -0
- package/skills/loop-agent/SKILL.md +260 -0
- package/skills/loop-agent/references/README.md +63 -0
- package/skills/loop-agent/references/command-reference.md +315 -0
- package/skills/loop-agent/references/harness-policy.md +258 -0
- package/skills/loop-agent/references/hybrid-dag.md +216 -0
- package/skills/loop-agent/references/learned/README.md +21 -0
- package/skills/loop-agent/references/model-routing.md +36 -0
- package/skills/loop-agent/references/multi-worktree.md +54 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -0
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
- package/skills/loop-agent/references/pi-prompt.md +23 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
- package/skills/loop-agent/references/task-workflow.md +84 -0
- package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/verification-before-completion/SKILL.md +154 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const ARTIFACTS_DIR = 'artifacts';
|
|
4
|
+
const SPEC_ARTIFACT_STEPS = ['analyze', 'plan'];
|
|
5
|
+
const DEFAULT_ARTIFACTS = {
|
|
6
|
+
'分析报告.md': '# 分析报告\n\n## 1. 任务目标\n\n## 2. 当前现状\n\n## 3. 受影响模块与文件\n\n## 4. 关键约束与不变式\n\n## 5. 硬约束摘要\n\n## 6. 允许修改范围 / 禁止修改范围\n\n## 7. 风险点\n\n## 8. 待确认问题\n',
|
|
7
|
+
'实现计划.md': '# 实现计划\n\n## 1. 修改目标\n\n## 2. 修改范围\n\n## 3. 允许修改文件 / 目录\n\n## 4. 明确不做的内容\n\n## 5. 关键决策\n\n## 6. 不变式\n\n## 7. 风险与回滚点\n\n## 8. 测试与验证方案\n\n## 9. 不在本次范围内的事项\n\n## 10. 待确认事项\n',
|
|
8
|
+
'修改记录.md': '# 修改记录\n\n## 1. 修改文件清单\n\n## 2. 关键改动说明\n',
|
|
9
|
+
'验证结果.md': '# 验证结果\n\n## 1. 执行的命令\n\n## 2. 通过项\n\n## 3. 失败项\n',
|
|
10
|
+
'复盘报告.md': '# 复盘报告\n\n## 1. 质量复盘\n\n## 2. 流程问题\n\n## 3. 下次建议\n',
|
|
11
|
+
};
|
|
12
|
+
const ARTIFACT_NAME_BY_STEP = {
|
|
13
|
+
analyze: '分析报告.md',
|
|
14
|
+
plan: '实现计划.md',
|
|
15
|
+
implement: '修改记录.md',
|
|
16
|
+
verify: '验证结果.md',
|
|
17
|
+
retrospective: '复盘报告.md',
|
|
18
|
+
};
|
|
19
|
+
export function getArtifactFileName(step) {
|
|
20
|
+
return ARTIFACT_NAME_BY_STEP[step];
|
|
21
|
+
}
|
|
22
|
+
export function getArtifactRelativePath(step) {
|
|
23
|
+
return `${ARTIFACTS_DIR}/${getArtifactFileName(step)}`;
|
|
24
|
+
}
|
|
25
|
+
/** Relative artifact path used in workflow evidence / executor records. */
|
|
26
|
+
export function getArtifactEvidence(step) {
|
|
27
|
+
return getArtifactRelativePath(step);
|
|
28
|
+
}
|
|
29
|
+
export function getArtifactDisplayName(step) {
|
|
30
|
+
return getArtifactFileName(step);
|
|
31
|
+
}
|
|
32
|
+
export function formatSpecArtifactRelativePaths() {
|
|
33
|
+
return SPEC_ARTIFACT_STEPS.map((step) => getArtifactRelativePath(step)).join(' + ');
|
|
34
|
+
}
|
|
35
|
+
export function formatSpecArtifactDisplayNames() {
|
|
36
|
+
return SPEC_ARTIFACT_STEPS.map((step) => getArtifactDisplayName(step)).join(' + ');
|
|
37
|
+
}
|
|
38
|
+
const REQUIRED_HEADINGS = {
|
|
39
|
+
analyze: ['# 分析报告', '## 1. 任务目标'],
|
|
40
|
+
plan: ['# 实现计划', '## 1. 修改目标'],
|
|
41
|
+
implement: ['# 修改记录', '## 1. 修改文件清单'],
|
|
42
|
+
retrospective: ['# 复盘报告', '## 1. 质量复盘'],
|
|
43
|
+
};
|
|
44
|
+
export async function initializeArtifacts(taskDir) {
|
|
45
|
+
const artifactsDir = path.join(taskDir, 'artifacts');
|
|
46
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
47
|
+
await Promise.all(Object.entries(DEFAULT_ARTIFACTS).map(([name, content]) => writeFile(path.join(artifactsDir, name), content, 'utf-8')));
|
|
48
|
+
}
|
|
49
|
+
export function getArtifactPath(taskDir, step) {
|
|
50
|
+
return path.join(taskDir, ARTIFACTS_DIR, getArtifactFileName(step));
|
|
51
|
+
}
|
|
52
|
+
export async function writeArtifactForStep(taskDir, step, content) {
|
|
53
|
+
await writeFile(getArtifactPath(taskDir, step), `${content.trim()}\n`, 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
function normalizeArtifactContent(content) {
|
|
56
|
+
return content.replace(/\r/g, '').trim().replace(/\s+/g, ' ');
|
|
57
|
+
}
|
|
58
|
+
function isUnchangedNewTaskTemplate(step, content) {
|
|
59
|
+
const artifactName = ARTIFACT_NAME_BY_STEP[step];
|
|
60
|
+
const template = DEFAULT_ARTIFACTS[artifactName];
|
|
61
|
+
return normalizeArtifactContent(content) === normalizeArtifactContent(template);
|
|
62
|
+
}
|
|
63
|
+
function firstRequiredSectionIsEmpty(step, content) {
|
|
64
|
+
const [, firstSectionHeading] = REQUIRED_HEADINGS[step];
|
|
65
|
+
const headingIndex = content.indexOf(firstSectionHeading);
|
|
66
|
+
if (headingIndex < 0) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const afterHeading = content.slice(headingIndex + firstSectionHeading.length);
|
|
70
|
+
const nextHeadingIndex = afterHeading.search(/^##\s/m);
|
|
71
|
+
const sectionBody = (nextHeadingIndex >= 0 ? afterHeading.slice(0, nextHeadingIndex) : afterHeading).trim();
|
|
72
|
+
return sectionBody.length === 0;
|
|
73
|
+
}
|
|
74
|
+
export function validateArtifactContent(step, content) {
|
|
75
|
+
const trimmed = content.trim();
|
|
76
|
+
if (!trimmed) {
|
|
77
|
+
return ['artifact content is empty'];
|
|
78
|
+
}
|
|
79
|
+
if (isUnchangedNewTaskTemplate(step, trimmed)) {
|
|
80
|
+
return ['artifact content matches unchanged new-task template'];
|
|
81
|
+
}
|
|
82
|
+
if (firstRequiredSectionIsEmpty(step, trimmed)) {
|
|
83
|
+
return ['artifact first required section has no substantive content'];
|
|
84
|
+
}
|
|
85
|
+
const missing = REQUIRED_HEADINGS[step]
|
|
86
|
+
.filter((heading) => !trimmed.includes(heading));
|
|
87
|
+
return missing.map((heading) => `missing required heading: ${heading}`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const DEFAULT_MAX_FILES = {
|
|
2
|
+
slim: 10,
|
|
3
|
+
full: 24,
|
|
4
|
+
};
|
|
5
|
+
export function buildContextFiles(groups, taskConfig) {
|
|
6
|
+
const merged = groups.flat();
|
|
7
|
+
const deduped = uniqueKeepOrder(merged);
|
|
8
|
+
const maxFiles = resolveContextMaxFiles(taskConfig);
|
|
9
|
+
if (maxFiles <= 0 || deduped.length <= maxFiles) {
|
|
10
|
+
return deduped;
|
|
11
|
+
}
|
|
12
|
+
return deduped.slice(0, maxFiles);
|
|
13
|
+
}
|
|
14
|
+
function uniqueKeepOrder(items) {
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const result = [];
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
if (seen.has(item))
|
|
19
|
+
continue;
|
|
20
|
+
seen.add(item);
|
|
21
|
+
result.push(item);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function resolveContextMaxFiles(taskConfig) {
|
|
26
|
+
if (taskConfig.contextMaxFiles > 0) {
|
|
27
|
+
return taskConfig.contextMaxFiles;
|
|
28
|
+
}
|
|
29
|
+
return taskConfig.contextProfile === 'slim'
|
|
30
|
+
? DEFAULT_MAX_FILES.slim
|
|
31
|
+
: DEFAULT_MAX_FILES.full;
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Recursively copy a directory tree. Symlinks are followed. */
|
|
4
|
+
export async function copyDir(src, dest) {
|
|
5
|
+
await mkdir(dest, { recursive: true });
|
|
6
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
7
|
+
for (const entry of entries) {
|
|
8
|
+
const srcPath = path.join(src, entry.name);
|
|
9
|
+
const destPath = path.join(dest, entry.name);
|
|
10
|
+
if (entry.isDirectory()) {
|
|
11
|
+
await copyDir(srcPath, destPath);
|
|
12
|
+
}
|
|
13
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
14
|
+
await copyFile(srcPath, destPath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export async function autoCommitVerifiedProgress(repoRoot, taskConfig) {
|
|
3
|
+
const statusOutput = await runGit(repoRoot, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
4
|
+
const entries = parseGitStatusPorcelain(statusOutput);
|
|
5
|
+
const changedPaths = Array.from(new Set(entries.map((entry) => entry.path)));
|
|
6
|
+
const stagedPaths = collectStagedPaths(entries);
|
|
7
|
+
const selectedPaths = selectAutoCommitPaths(changedPaths, taskConfig, stagedPaths);
|
|
8
|
+
const skippedPaths = changedPaths.filter((filePath) => !selectedPaths.includes(filePath));
|
|
9
|
+
const commitMessage = buildAutoCommitMessage(taskConfig);
|
|
10
|
+
if (selectedPaths.length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
committed: false,
|
|
13
|
+
commitMessage,
|
|
14
|
+
selectedPaths,
|
|
15
|
+
skippedPaths,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
await runGit(repoRoot, ['add', '--', ...selectedPaths]);
|
|
19
|
+
const stagedOutput = await runGit(repoRoot, ['diff', '--cached', '--name-only', '--', ...selectedPaths]);
|
|
20
|
+
const confirmedStagedPaths = stagedOutput
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map((line) => line.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
if (confirmedStagedPaths.length === 0) {
|
|
25
|
+
return {
|
|
26
|
+
committed: false,
|
|
27
|
+
commitMessage,
|
|
28
|
+
selectedPaths,
|
|
29
|
+
skippedPaths,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
await runGit(repoRoot, ['commit', '--only', '-m', commitMessage, '--', ...selectedPaths]);
|
|
33
|
+
const commitHash = (await runGit(repoRoot, ['rev-parse', 'HEAD'])).trim();
|
|
34
|
+
return {
|
|
35
|
+
committed: true,
|
|
36
|
+
commitHash,
|
|
37
|
+
commitMessage,
|
|
38
|
+
selectedPaths,
|
|
39
|
+
skippedPaths,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function parseGitStatusPorcelain(output) {
|
|
43
|
+
return output
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((line) => line.trimEnd())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((line) => {
|
|
48
|
+
const x = line[0] ?? ' ';
|
|
49
|
+
const y = line[1] ?? ' ';
|
|
50
|
+
const rawPath = line.slice(3);
|
|
51
|
+
const path = normalizeGitPath(rawPath);
|
|
52
|
+
return { path, x, y };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function selectAutoCommitPaths(changedPaths, taskConfig, stagedPaths) {
|
|
56
|
+
const allowed = taskConfig.allowedPaths ?? [];
|
|
57
|
+
const forbidden = taskConfig.forbiddenPaths ?? [];
|
|
58
|
+
const hasAllowed = allowed.length > 0;
|
|
59
|
+
return changedPaths.filter((filePath) => {
|
|
60
|
+
const normalizedPath = normalizePath(filePath);
|
|
61
|
+
const forbiddenMatch = matchesAnyPattern(normalizedPath, forbidden);
|
|
62
|
+
if (forbiddenMatch)
|
|
63
|
+
return false;
|
|
64
|
+
// Already-staged paths are accepted as implicit authorisation by the
|
|
65
|
+
// implement step (e.g. `git mv` / `git rm` preformed before verify).
|
|
66
|
+
// forbiddenPaths still applies as a hard guard.
|
|
67
|
+
if (stagedPaths?.has(filePath))
|
|
68
|
+
return true;
|
|
69
|
+
const allowedMatch = hasAllowed
|
|
70
|
+
? matchesAnyPattern(normalizedPath, allowed)
|
|
71
|
+
: !normalizedPath.startsWith('.harness/tasks/');
|
|
72
|
+
return allowedMatch;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Collect paths that were already staged (modified, added, deleted, renamed,
|
|
77
|
+
* or copied in the index) before auto-commit runs. Untracked files (`x === '?'`)
|
|
78
|
+
* are NOT staged; they require explicit allowedPaths approval.
|
|
79
|
+
*/
|
|
80
|
+
export function collectStagedPaths(entries) {
|
|
81
|
+
const staged = new Set();
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry.x !== ' ' && entry.x !== '?') {
|
|
84
|
+
staged.add(entry.path);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return staged;
|
|
88
|
+
}
|
|
89
|
+
export function buildAutoCommitMessage(taskConfig) {
|
|
90
|
+
if (taskConfig.autoCommitMessage?.trim()) {
|
|
91
|
+
return taskConfig.autoCommitMessage.trim();
|
|
92
|
+
}
|
|
93
|
+
return `chore(task): save verified progress for ${taskConfig.taskId}`;
|
|
94
|
+
}
|
|
95
|
+
function matchesAnyPattern(filePath, patterns) {
|
|
96
|
+
return patterns.some((pattern) => pathMatchesPattern(filePath, pattern));
|
|
97
|
+
}
|
|
98
|
+
export function pathMatchesPattern(filePath, pattern) {
|
|
99
|
+
const normalizedPath = normalizePath(filePath);
|
|
100
|
+
let normalizedPattern = normalizePath(pattern);
|
|
101
|
+
if (normalizedPattern.endsWith('/')) {
|
|
102
|
+
normalizedPattern = normalizedPattern.slice(0, -1);
|
|
103
|
+
}
|
|
104
|
+
if (normalizedPattern.includes('*')) {
|
|
105
|
+
return globToRegExp(normalizedPattern).test(normalizedPath);
|
|
106
|
+
}
|
|
107
|
+
// Directory-style allowlist entries should match nested files too.
|
|
108
|
+
return normalizedPath === normalizedPattern || normalizedPath.startsWith(`${normalizedPattern}/`);
|
|
109
|
+
}
|
|
110
|
+
function globToRegExp(pattern) {
|
|
111
|
+
const normalized = normalizePath(pattern);
|
|
112
|
+
let regex = '^';
|
|
113
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
114
|
+
const char = normalized[i];
|
|
115
|
+
const next = normalized[i + 1];
|
|
116
|
+
if (char === '*' && next === '*') {
|
|
117
|
+
regex += '.*';
|
|
118
|
+
i += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (char === '*') {
|
|
122
|
+
regex += '[^/]*';
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
regex += escapeRegExp(char);
|
|
126
|
+
}
|
|
127
|
+
regex += '$';
|
|
128
|
+
return new RegExp(regex);
|
|
129
|
+
}
|
|
130
|
+
function escapeRegExp(char) {
|
|
131
|
+
return /[\\^$.*+?()[\]{}|]/.test(char) ? `\\${char}` : char;
|
|
132
|
+
}
|
|
133
|
+
function normalizeGitPath(value) {
|
|
134
|
+
const target = value.includes(' -> ') ? value.split(' -> ').at(-1) ?? value : value;
|
|
135
|
+
return normalizePath(target);
|
|
136
|
+
}
|
|
137
|
+
function normalizePath(value) {
|
|
138
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
139
|
+
}
|
|
140
|
+
async function runGit(repoRoot, args) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const child = spawn('git', args, {
|
|
143
|
+
cwd: repoRoot,
|
|
144
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
145
|
+
});
|
|
146
|
+
let stdout = '';
|
|
147
|
+
let stderr = '';
|
|
148
|
+
child.stdout.setEncoding('utf-8');
|
|
149
|
+
child.stderr.setEncoding('utf-8');
|
|
150
|
+
child.stdout.on('data', (chunk) => {
|
|
151
|
+
stdout += chunk;
|
|
152
|
+
});
|
|
153
|
+
child.stderr.on('data', (chunk) => {
|
|
154
|
+
stderr += chunk;
|
|
155
|
+
});
|
|
156
|
+
child.on('error', reject);
|
|
157
|
+
child.on('close', (code) => {
|
|
158
|
+
if (code === 0) {
|
|
159
|
+
resolve(stdout);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
reject(new Error(`git ${args.join(' ')} failed: ${(stderr || stdout).trim()}`));
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function appendWorkflowLog(taskDir, message) {
|
|
4
|
+
const logsDir = path.join(taskDir, 'logs');
|
|
5
|
+
await mkdir(logsDir, { recursive: true });
|
|
6
|
+
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
7
|
+
await appendFile(path.join(logsDir, 'workflow.log'), line, 'utf-8');
|
|
8
|
+
}
|
|
9
|
+
export async function appendExecutorRecord(taskDir, record) {
|
|
10
|
+
const logsDir = path.join(taskDir, 'logs');
|
|
11
|
+
await mkdir(logsDir, { recursive: true });
|
|
12
|
+
await appendFile(path.join(logsDir, 'executor.jsonl'), `${JSON.stringify(record)}\n`, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
export async function appendDecisionRecord(taskDir, record) {
|
|
15
|
+
const logsDir = path.join(taskDir, 'logs');
|
|
16
|
+
await mkdir(logsDir, { recursive: true });
|
|
17
|
+
await appendFile(path.join(logsDir, 'decisions.jsonl'), `${JSON.stringify(record)}\n`, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
export async function appendGoalEventRecord(taskDir, record) {
|
|
20
|
+
const logsDir = path.join(taskDir, 'logs');
|
|
21
|
+
await mkdir(logsDir, { recursive: true });
|
|
22
|
+
await appendFile(path.join(logsDir, 'goal-events.jsonl'), `${JSON.stringify(record)}\n`, 'utf-8');
|
|
23
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export function createOneShotPromptParseAccum() {
|
|
4
|
+
return {
|
|
5
|
+
fromStdin: false,
|
|
6
|
+
positional: [],
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function parseOneShotCommonFlag(args, index, accum) {
|
|
10
|
+
const arg = args[index];
|
|
11
|
+
if (arg === "--stdin") {
|
|
12
|
+
accum.fromStdin = true;
|
|
13
|
+
return { kind: "consumed", nextIndex: index };
|
|
14
|
+
}
|
|
15
|
+
if (arg === "--file") {
|
|
16
|
+
const path = args[index + 1];
|
|
17
|
+
if (!path)
|
|
18
|
+
throw new Error("--file requires a path");
|
|
19
|
+
accum.fromFile = path;
|
|
20
|
+
return { kind: "consumed", nextIndex: index + 1 };
|
|
21
|
+
}
|
|
22
|
+
if (arg === "--cwd") {
|
|
23
|
+
const path = args[index + 1];
|
|
24
|
+
if (!path)
|
|
25
|
+
throw new Error("--cwd requires a path");
|
|
26
|
+
accum.cwd = path;
|
|
27
|
+
return { kind: "consumed", nextIndex: index + 1 };
|
|
28
|
+
}
|
|
29
|
+
if (arg === "--timeout") {
|
|
30
|
+
const val = args[index + 1];
|
|
31
|
+
if (!val)
|
|
32
|
+
throw new Error("--timeout requires a value in ms");
|
|
33
|
+
accum.timeoutMs = parsePositiveTimeoutMs(val);
|
|
34
|
+
return { kind: "consumed", nextIndex: index + 1 };
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--help" || arg === "-h") {
|
|
37
|
+
return { kind: "help" };
|
|
38
|
+
}
|
|
39
|
+
if (!arg.startsWith("--")) {
|
|
40
|
+
accum.positional.push(arg);
|
|
41
|
+
return { kind: "consumed", nextIndex: index };
|
|
42
|
+
}
|
|
43
|
+
return { kind: "not-common" };
|
|
44
|
+
}
|
|
45
|
+
export function parsePositiveTimeoutMs(val) {
|
|
46
|
+
const timeoutMs = Number.parseInt(val, 10);
|
|
47
|
+
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
48
|
+
throw new Error(`invalid timeout: ${val}`);
|
|
49
|
+
}
|
|
50
|
+
return timeoutMs;
|
|
51
|
+
}
|
|
52
|
+
export function resolveOneShotTask(accum, usage) {
|
|
53
|
+
let task = "";
|
|
54
|
+
if (accum.fromStdin) {
|
|
55
|
+
task = readFileSync(process.stdin.fd, "utf-8").trim();
|
|
56
|
+
}
|
|
57
|
+
else if (accum.fromFile) {
|
|
58
|
+
task = readFileSync(resolve(accum.fromFile), "utf-8").trim();
|
|
59
|
+
}
|
|
60
|
+
else if (accum.positional.length > 0) {
|
|
61
|
+
task = accum.positional.join(" ");
|
|
62
|
+
}
|
|
63
|
+
if (!task.trim()) {
|
|
64
|
+
throw formatNoTaskError(usage);
|
|
65
|
+
}
|
|
66
|
+
return task;
|
|
67
|
+
}
|
|
68
|
+
export function resolveOneShotCwd(cwd) {
|
|
69
|
+
return cwd ? resolve(cwd) : undefined;
|
|
70
|
+
}
|
|
71
|
+
export function formatUnknownFlagError(flag, usage) {
|
|
72
|
+
return new Error(`unknown flag: ${flag}\n${usage}`);
|
|
73
|
+
}
|
|
74
|
+
export function formatNoTaskError(usage) {
|
|
75
|
+
return new Error(`no task provided\n${usage}`);
|
|
76
|
+
}
|
|
77
|
+
export function printOneShotHelpAndExit(usage) {
|
|
78
|
+
console.log(usage);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
export function handleStandaloneOneShotCommandError(err, printUsage) {
|
|
82
|
+
if (!(err instanceof Error)) {
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
if (err.message.startsWith("no task provided")) {
|
|
86
|
+
printUsage();
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
if (err.message.startsWith("unknown flag")) {
|
|
90
|
+
process.stderr.write(`${err.message}\n`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
if (err.message.startsWith("usage:")) {
|
|
94
|
+
process.stderr.write(`${err.message}\n`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function toPosixPath(value) {
|
|
3
|
+
return value.replace(/\\/g, "/");
|
|
4
|
+
}
|
|
5
|
+
export function repoRelativePath(repoRoot, filePath) {
|
|
6
|
+
const relative = path.relative(repoRoot, filePath) || ".";
|
|
7
|
+
return toPosixPath(relative);
|
|
8
|
+
}
|
|
9
|
+
export function referencePathJoin(...parts) {
|
|
10
|
+
return path.posix.join(...parts.map(toPosixPath));
|
|
11
|
+
}
|
|
12
|
+
export function resolveCliPath(value, cwd = process.cwd()) {
|
|
13
|
+
if (process.platform === "win32" && value.startsWith("/") && !value.startsWith("//")) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
17
|
+
}
|
|
18
|
+
export function rewritePathPrefix(value, oldPrefix, newPrefix) {
|
|
19
|
+
const normalizedValue = toPosixPath(value);
|
|
20
|
+
const normalizedOld = toPosixPath(oldPrefix).replace(/\/+$/, "");
|
|
21
|
+
const normalizedNew = toPosixPath(newPrefix).replace(/\/+$/, "");
|
|
22
|
+
const toNewPrefixStyle = (rewritten) => value.includes("\\") || newPrefix.includes("\\")
|
|
23
|
+
? rewritten.replace(/\//g, "\\")
|
|
24
|
+
: rewritten;
|
|
25
|
+
if (normalizedValue === normalizedOld)
|
|
26
|
+
return toNewPrefixStyle(normalizedNew);
|
|
27
|
+
if (normalizedValue.startsWith(`${normalizedOld}/`)) {
|
|
28
|
+
return toNewPrefixStyle(`${normalizedNew}${normalizedValue.slice(normalizedOld.length)}`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isFeatureStudyTask } from './reference-context.js';
|
|
4
|
+
export async function loadPrompt(repoRoot, step, taskConfig) {
|
|
5
|
+
const filename = resolvePromptFilename(step, taskConfig);
|
|
6
|
+
const promptPath = path.join(repoRoot, '.harness', 'prompts', filename);
|
|
7
|
+
try {
|
|
8
|
+
return await readFile(promptPath, 'utf-8');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
if (filename !== `${step}.md`) {
|
|
12
|
+
return readFile(path.join(repoRoot, '.harness', 'prompts', `${step}.md`), 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`prompt not found: ${promptPath}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function resolvePromptFilename(step, taskConfig) {
|
|
18
|
+
if (!taskConfig || !isFeatureStudyTask(taskConfig)) {
|
|
19
|
+
return `${step}.md`;
|
|
20
|
+
}
|
|
21
|
+
if (step === 'analyze')
|
|
22
|
+
return 'feature-study-analyze.md';
|
|
23
|
+
if (step === 'plan')
|
|
24
|
+
return 'feature-study-plan.md';
|
|
25
|
+
return `${step}.md`;
|
|
26
|
+
}
|