@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,699 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { initializeArtifacts } from '../shared/artifacts-core.js';
|
|
6
|
+
import { appendDecisionRecord, appendGoalEventRecord, appendWorkflowLog } from '../shared/logger.js';
|
|
7
|
+
import { loadWorkflowState, saveWorkflowState } from './state.js';
|
|
8
|
+
import { taskConfigSchema, taskGoalStatusSchema, } from '../shared/types.js';
|
|
9
|
+
const STEP_ORDER = ['analyze', 'plan', 'implement', 'verify', 'retrospective'];
|
|
10
|
+
/** Canonical task id: YYYY-MM-DD-<kebab-slug> using local business date at creation time. */
|
|
11
|
+
export const CANONICAL_TASK_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
12
|
+
const GENERIC_TASK_SLUGS = new Set(['test', 'fix', 'tmp', 'new-task']);
|
|
13
|
+
/** Local calendar date as YYYY-MM-DD (not UTC-derived). */
|
|
14
|
+
export function formatLocalBusinessDate(date = new Date()) {
|
|
15
|
+
const year = date.getFullYear();
|
|
16
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
17
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
18
|
+
return `${year}-${month}-${day}`;
|
|
19
|
+
}
|
|
20
|
+
/** Compact local calendar date as YYYYMMDD for DAG run ids. */
|
|
21
|
+
export function formatLocalCompactDate(date = new Date()) {
|
|
22
|
+
return formatLocalBusinessDate(date).replace(/-/g, '');
|
|
23
|
+
}
|
|
24
|
+
export function validateNewTaskId(taskId) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
const warnings = [];
|
|
27
|
+
const trimmed = taskId.trim();
|
|
28
|
+
if (!trimmed) {
|
|
29
|
+
errors.push('taskId cannot be empty');
|
|
30
|
+
return { errors, warnings };
|
|
31
|
+
}
|
|
32
|
+
if (CANONICAL_TASK_ID_PATTERN.test(trimmed)) {
|
|
33
|
+
const slug = trimmed.slice(11);
|
|
34
|
+
const slugBase = slug.replace(/-r\d+$/, '');
|
|
35
|
+
if (GENERIC_TASK_SLUGS.has(slug) || GENERIC_TASK_SLUGS.has(slugBase)) {
|
|
36
|
+
errors.push(`taskId slug "${slug}" is too generic; use a descriptive kebab-slug instead of test/fix/tmp/new-task`);
|
|
37
|
+
}
|
|
38
|
+
return { errors, warnings };
|
|
39
|
+
}
|
|
40
|
+
warnings.push(`taskId "${trimmed}" does not match canonical YYYY-MM-DD-<slug>; legacy ids remain readable but new tasks should use a local business date prefix`);
|
|
41
|
+
const legacyBase = trimmed.replace(/-r\d+$/, '');
|
|
42
|
+
if (GENERIC_TASK_SLUGS.has(trimmed) || GENERIC_TASK_SLUGS.has(legacyBase)) {
|
|
43
|
+
errors.push(`taskId "${trimmed}" is too generic; use a descriptive kebab-slug`);
|
|
44
|
+
}
|
|
45
|
+
return { errors, warnings };
|
|
46
|
+
}
|
|
47
|
+
const ARTIFACT_KEY_BY_STEP = {
|
|
48
|
+
analyze: 'analysis',
|
|
49
|
+
plan: 'plan',
|
|
50
|
+
implement: 'modifyLog',
|
|
51
|
+
verify: 'verifyResult',
|
|
52
|
+
retrospective: 'retrospective',
|
|
53
|
+
};
|
|
54
|
+
const PLANNING_STEPS = new Set(['plan', 'spec']);
|
|
55
|
+
const MAX_DECISION_LOG_ENTRIES = 50;
|
|
56
|
+
export function getTaskDir(repoRoot, taskId) {
|
|
57
|
+
return path.join(repoRoot, '.harness', 'tasks', taskId);
|
|
58
|
+
}
|
|
59
|
+
export function getTaskPaths(repoRoot, taskId) {
|
|
60
|
+
const taskDir = getTaskDir(repoRoot, taskId);
|
|
61
|
+
return {
|
|
62
|
+
taskDir,
|
|
63
|
+
taskConfigPath: path.join(taskDir, 'task.json'),
|
|
64
|
+
logsDir: path.join(taskDir, 'logs'),
|
|
65
|
+
sourceDir: path.join(taskDir, 'source'),
|
|
66
|
+
statePath: path.join(taskDir, '.workflow_state.json'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function createTask(repoRoot, taskId, title) {
|
|
70
|
+
const validation = validateNewTaskId(taskId);
|
|
71
|
+
for (const warning of validation.warnings) {
|
|
72
|
+
console.warn(`[new-task] warning: ${warning}`);
|
|
73
|
+
}
|
|
74
|
+
if (validation.errors.length > 0) {
|
|
75
|
+
throw new Error(`invalid taskId: ${validation.errors.join('; ')}`);
|
|
76
|
+
}
|
|
77
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
78
|
+
try {
|
|
79
|
+
await access(paths.statePath);
|
|
80
|
+
throw new Error(`task already exists: ${taskId}`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const isMissing = typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
|
|
84
|
+
if (!isMissing)
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
await mkdir(paths.sourceDir, { recursive: true });
|
|
88
|
+
await mkdir(paths.logsDir, { recursive: true });
|
|
89
|
+
await initializeArtifacts(paths.taskDir);
|
|
90
|
+
const taskConfig = {
|
|
91
|
+
taskId,
|
|
92
|
+
title: title ?? taskId,
|
|
93
|
+
sourceFiles: [],
|
|
94
|
+
taskKind: 'standard',
|
|
95
|
+
referenceRepos: [],
|
|
96
|
+
referenceDocs: [],
|
|
97
|
+
allowedPaths: [],
|
|
98
|
+
forbiddenPaths: [],
|
|
99
|
+
hardConstraints: [],
|
|
100
|
+
autoCommitAfterVerify: true,
|
|
101
|
+
autoCommitMessage: '',
|
|
102
|
+
complexity: 'medium',
|
|
103
|
+
dagFallbackReason: '',
|
|
104
|
+
contextProfile: 'full',
|
|
105
|
+
verifyMode: 'parallel',
|
|
106
|
+
verifyPreset: 'auto',
|
|
107
|
+
verifyQuota: 'full',
|
|
108
|
+
verifyRetryCount: 0,
|
|
109
|
+
verifyFailFast: false,
|
|
110
|
+
contextMaxFiles: 0,
|
|
111
|
+
requireRetrospective: false,
|
|
112
|
+
convergence: {
|
|
113
|
+
enabled: false,
|
|
114
|
+
maxPasses: 3,
|
|
115
|
+
stopOnVerdictPass: true,
|
|
116
|
+
stopOnHardVerifyPass: true,
|
|
117
|
+
pauseOnRegression: true,
|
|
118
|
+
},
|
|
119
|
+
loopAutoWritePolicy: 'off',
|
|
120
|
+
verifyEnv: 'clean',
|
|
121
|
+
maxGoalContinuationsPerRun: 5,
|
|
122
|
+
piSubagentMode: 'off',
|
|
123
|
+
executor: 'pi',
|
|
124
|
+
notes: '',
|
|
125
|
+
};
|
|
126
|
+
const state = {
|
|
127
|
+
taskId,
|
|
128
|
+
status: 'draft',
|
|
129
|
+
currentStep: 'analyze',
|
|
130
|
+
completedSteps: [],
|
|
131
|
+
lastUpdated: new Date().toISOString(),
|
|
132
|
+
executor: {
|
|
133
|
+
name: 'pi',
|
|
134
|
+
mode: 'json',
|
|
135
|
+
},
|
|
136
|
+
artifacts: {
|
|
137
|
+
analysis: false,
|
|
138
|
+
plan: false,
|
|
139
|
+
modifyLog: false,
|
|
140
|
+
verifyResult: false,
|
|
141
|
+
retrospective: false,
|
|
142
|
+
},
|
|
143
|
+
sourceState: {
|
|
144
|
+
currentHash: '',
|
|
145
|
+
plannedHash: '',
|
|
146
|
+
trackedFiles: [],
|
|
147
|
+
lastCheckedAt: '',
|
|
148
|
+
lastPlannedAt: '',
|
|
149
|
+
lastPlannedStep: null,
|
|
150
|
+
stalePlan: false,
|
|
151
|
+
},
|
|
152
|
+
goal: {
|
|
153
|
+
objective: '',
|
|
154
|
+
status: 'paused',
|
|
155
|
+
tokenBudget: null,
|
|
156
|
+
tokensUsed: 0,
|
|
157
|
+
timeUsedSeconds: 0,
|
|
158
|
+
pendingObjectiveUpdatedNotice: false,
|
|
159
|
+
pendingBudgetLimitNotice: false,
|
|
160
|
+
continuationRuns: 0,
|
|
161
|
+
createdAt: '',
|
|
162
|
+
updatedAt: '',
|
|
163
|
+
},
|
|
164
|
+
decisionLog: [],
|
|
165
|
+
};
|
|
166
|
+
await writeFile(paths.taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}\n`, 'utf-8');
|
|
167
|
+
await saveWorkflowState(paths.statePath, state);
|
|
168
|
+
await writeFile(path.join(paths.logsDir, 'executor.jsonl'), '', 'utf-8');
|
|
169
|
+
await writeFile(path.join(paths.logsDir, 'decisions.jsonl'), '', 'utf-8');
|
|
170
|
+
await appendWorkflowLog(paths.taskDir, `created task ${taskId}`);
|
|
171
|
+
return paths.taskDir;
|
|
172
|
+
}
|
|
173
|
+
export async function getTaskStatus(repoRoot, taskId) {
|
|
174
|
+
return loadWorkflowState(getTaskPaths(repoRoot, taskId).statePath);
|
|
175
|
+
}
|
|
176
|
+
export async function loadTaskConfig(repoRoot, taskId) {
|
|
177
|
+
const raw = await readFile(getTaskPaths(repoRoot, taskId).taskConfigPath, 'utf-8');
|
|
178
|
+
return parseTaskConfigForRuntime(JSON.parse(raw));
|
|
179
|
+
}
|
|
180
|
+
export function parseTaskConfigForRuntime(rawConfig) {
|
|
181
|
+
const parsed = taskConfigSchema.parse(rawConfig);
|
|
182
|
+
if (typeof rawConfig === 'object' &&
|
|
183
|
+
rawConfig !== null &&
|
|
184
|
+
!Object.prototype.hasOwnProperty.call(rawConfig, 'maxFixLoops')) {
|
|
185
|
+
return {
|
|
186
|
+
...parsed,
|
|
187
|
+
maxFixLoops: deriveMaxFixLoops(parsed.complexity),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return parsed;
|
|
191
|
+
}
|
|
192
|
+
export function deriveMaxFixLoops(complexity) {
|
|
193
|
+
if (complexity === 'small')
|
|
194
|
+
return 2;
|
|
195
|
+
return 3;
|
|
196
|
+
}
|
|
197
|
+
export async function listTaskSourceFiles(repoRoot, taskId) {
|
|
198
|
+
return listFilesRecursive(getTaskPaths(repoRoot, taskId).sourceDir);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Resolve tool names for a given step and piSubagentMode.
|
|
202
|
+
*
|
|
203
|
+
* - `off`: read-only tools for non-implement steps, standard tools for implement
|
|
204
|
+
* - `analyze-plan`: as `off` plus `subagent` for analyze/plan/spec
|
|
205
|
+
* - `full`: as `analyze-plan` plus `subagent` for retrospective
|
|
206
|
+
* - implement never gets `subagent` in any mode (avoids nested multi-writer)
|
|
207
|
+
*/
|
|
208
|
+
export function resolvePiSubagentTools(step, mode) {
|
|
209
|
+
const readOnlyTools = ['read', 'grep', 'find', 'ls'];
|
|
210
|
+
const implementTools = ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'];
|
|
211
|
+
if (step === 'implement') {
|
|
212
|
+
return implementTools;
|
|
213
|
+
}
|
|
214
|
+
if (mode === 'off') {
|
|
215
|
+
return readOnlyTools;
|
|
216
|
+
}
|
|
217
|
+
const shouldHaveSubagent = step === 'analyze' || step === 'plan' || step === 'spec' ||
|
|
218
|
+
(mode === 'full' && step === 'retrospective');
|
|
219
|
+
if (shouldHaveSubagent) {
|
|
220
|
+
return [...readOnlyTools, 'subagent'];
|
|
221
|
+
}
|
|
222
|
+
return readOnlyTools;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Determine whether subagent guidance should be injected into the user message
|
|
226
|
+
* for a given step and piSubagentMode.
|
|
227
|
+
*/
|
|
228
|
+
export function shouldInjectSubagentGuidance(step, mode) {
|
|
229
|
+
if (mode === 'off')
|
|
230
|
+
return false;
|
|
231
|
+
if (step === 'implement')
|
|
232
|
+
return false;
|
|
233
|
+
if (mode === 'analyze-plan' && (step === 'analyze' || step === 'plan' || step === 'spec'))
|
|
234
|
+
return true;
|
|
235
|
+
if (mode === 'full' && (step === 'analyze' || step === 'plan' || step === 'spec' || step === 'retrospective'))
|
|
236
|
+
return true;
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
/** Advisory guidance for `analyze-plan` mode. */
|
|
240
|
+
export const SUBAGENT_GUIDANCE_STANDARD = `<subagent_guidance>
|
|
241
|
+
You have access to the \`subagent\` tool for lightweight delegation within this step.
|
|
242
|
+
Use it only for read-only tasks:
|
|
243
|
+
- Parallel scout: dispatch multiple subagents to search/read different areas simultaneously
|
|
244
|
+
- Chain: scout -> planner (one subagent scouts, another plans based on findings)
|
|
245
|
+
- Reviewer: have a subagent review your analysis/plan before finalizing
|
|
246
|
+
Do NOT use subagent for writing, editing, or executing commands.
|
|
247
|
+
Subagent output is advisory only; always verify and incorporate findings into your own output.
|
|
248
|
+
Do NOT treat subagent results as authoritative state or artifact sources.
|
|
249
|
+
</subagent_guidance>`;
|
|
250
|
+
/** Strong guidance for `full` mode — prescriptive when to delegate. */
|
|
251
|
+
export const SUBAGENT_GUIDANCE_STRONG = `<subagent_guidance>
|
|
252
|
+
You have access to the \`subagent\` tool for lightweight delegation within this step.
|
|
253
|
+
|
|
254
|
+
You SHOULD delegate to subagent scouts when:
|
|
255
|
+
- The task requires scanning 3+ directories or comparing implementations across modules
|
|
256
|
+
- You would otherwise need 5+ sequential read/grep calls to gather context
|
|
257
|
+
- A reviewer subagent can independently catch scope drift before you finalize your output
|
|
258
|
+
|
|
259
|
+
Delegation saves context tokens and produces better results.
|
|
260
|
+
|
|
261
|
+
Allowed patterns:
|
|
262
|
+
- Parallel scout: dispatch 2-3 subagents simultaneously to cover different file trees
|
|
263
|
+
- Chain: scout -> planner (one subagent scouts, another plans based on findings)
|
|
264
|
+
- Reviewer: have a subagent review your analysis/plan before finalizing
|
|
265
|
+
|
|
266
|
+
Do NOT use subagent for writing, editing, or executing commands.
|
|
267
|
+
Subagent output is advisory only; always verify and incorporate findings into your own output.
|
|
268
|
+
Do NOT treat subagent results as authoritative state or artifact sources.
|
|
269
|
+
</subagent_guidance>`;
|
|
270
|
+
/** Preserved for backward compatibility (alias of STANDARD). */
|
|
271
|
+
export const SUBAGENT_GUIDANCE = SUBAGENT_GUIDANCE_STANDARD;
|
|
272
|
+
/** Resolve the correct guidance text for a given piSubagentMode. */
|
|
273
|
+
export function resolveSubagentGuidance(mode) {
|
|
274
|
+
return mode === 'full' ? SUBAGENT_GUIDANCE_STRONG : SUBAGENT_GUIDANCE_STANDARD;
|
|
275
|
+
}
|
|
276
|
+
export function getNextStep(step) {
|
|
277
|
+
if (step === 'spec') {
|
|
278
|
+
return 'implement';
|
|
279
|
+
}
|
|
280
|
+
const currentIndex = STEP_ORDER.indexOf(step);
|
|
281
|
+
return STEP_ORDER[Math.min(currentIndex + 1, STEP_ORDER.length - 1)] ?? 'retrospective';
|
|
282
|
+
}
|
|
283
|
+
export function getSupportedNextCommand(step, taskId, status, options) {
|
|
284
|
+
const hasActiveGoal = Boolean(options?.goal?.objective) && options?.goal?.status === 'active';
|
|
285
|
+
if (status === 'completed') {
|
|
286
|
+
return `task ${taskId} completed`;
|
|
287
|
+
}
|
|
288
|
+
if (hasActiveGoal) {
|
|
289
|
+
return `npm run dev -- loop status ${taskId}`;
|
|
290
|
+
}
|
|
291
|
+
if (step === 'analyze' || step === 'plan' || step === 'spec' || step === 'implement' || step === 'verify' || step === 'retrospective') {
|
|
292
|
+
const dagPath = path.join(os.tmpdir(), `${taskId}-dag.json`);
|
|
293
|
+
return `npm run dev -- dag run-task ${taskId} --profile auto --strict-models --output ${JSON.stringify(dagPath)}`;
|
|
294
|
+
}
|
|
295
|
+
return `${step} step not yet implemented in this prototype; human intervention required`;
|
|
296
|
+
}
|
|
297
|
+
export async function refreshSourceState(repoRoot, taskId) {
|
|
298
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
299
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
300
|
+
state.sourceState = await computeSourceState(paths.sourceDir, state.sourceState);
|
|
301
|
+
state.lastUpdated = new Date().toISOString();
|
|
302
|
+
await saveWorkflowState(paths.statePath, state);
|
|
303
|
+
return state;
|
|
304
|
+
}
|
|
305
|
+
export async function assertPlanFreshForImplement(repoRoot, taskId) {
|
|
306
|
+
const state = await refreshSourceState(repoRoot, taskId);
|
|
307
|
+
if (!state.sourceState.plannedHash) {
|
|
308
|
+
throw new Error('implement requires a completed plan/spec baseline before execution');
|
|
309
|
+
}
|
|
310
|
+
if (state.sourceState.stalePlan) {
|
|
311
|
+
throw new Error('source materials changed after the last plan/spec; rerun plan or spec before implement');
|
|
312
|
+
}
|
|
313
|
+
return state;
|
|
314
|
+
}
|
|
315
|
+
export async function markTaskRunning(repoRoot, taskId, step, detail) {
|
|
316
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
317
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
318
|
+
const previousStatus = state.status;
|
|
319
|
+
state.sourceState = await computeSourceState(paths.sourceDir, state.sourceState);
|
|
320
|
+
state.status = 'active';
|
|
321
|
+
state.currentStep = step;
|
|
322
|
+
state.lastUpdated = new Date().toISOString();
|
|
323
|
+
await recordDecision(state, paths.taskDir, {
|
|
324
|
+
time: state.lastUpdated,
|
|
325
|
+
step,
|
|
326
|
+
fromStatus: previousStatus,
|
|
327
|
+
toStatus: 'active',
|
|
328
|
+
reason: detail?.reason ?? `step ${step} started`,
|
|
329
|
+
evidence: detail?.evidence ?? '',
|
|
330
|
+
});
|
|
331
|
+
await saveWorkflowState(paths.statePath, state);
|
|
332
|
+
}
|
|
333
|
+
export async function markTaskFailed(repoRoot, taskId, step, detail) {
|
|
334
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
335
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
336
|
+
const previousStatus = state.status;
|
|
337
|
+
state.sourceState = await computeSourceState(paths.sourceDir, state.sourceState);
|
|
338
|
+
state.status = 'failed';
|
|
339
|
+
state.currentStep = step;
|
|
340
|
+
state.lastUpdated = new Date().toISOString();
|
|
341
|
+
await recordDecision(state, paths.taskDir, {
|
|
342
|
+
time: state.lastUpdated,
|
|
343
|
+
step,
|
|
344
|
+
fromStatus: previousStatus,
|
|
345
|
+
toStatus: 'failed',
|
|
346
|
+
reason: detail?.reason ?? `step ${step} failed`,
|
|
347
|
+
evidence: detail?.evidence ?? '',
|
|
348
|
+
});
|
|
349
|
+
await saveWorkflowState(paths.statePath, state);
|
|
350
|
+
}
|
|
351
|
+
export async function markStepCompleted(repoRoot, taskId, step, detail) {
|
|
352
|
+
await updateStepCompletion(repoRoot, taskId, step, false, detail);
|
|
353
|
+
}
|
|
354
|
+
export async function markTaskCompleted(repoRoot, taskId, step, detail) {
|
|
355
|
+
await updateStepCompletion(repoRoot, taskId, step, true, detail);
|
|
356
|
+
}
|
|
357
|
+
export async function markVerifyGoalPending(repoRoot, taskId, detail) {
|
|
358
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
359
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
360
|
+
const previousStatus = state.status;
|
|
361
|
+
state.sourceState = await computeSourceState(paths.sourceDir, state.sourceState);
|
|
362
|
+
state.status = 'active';
|
|
363
|
+
state.currentStep = 'implement';
|
|
364
|
+
state.lastUpdated = new Date().toISOString();
|
|
365
|
+
state.completedSteps = Array.from(new Set([...state.completedSteps, 'verify']));
|
|
366
|
+
state.artifacts.verifyResult = true;
|
|
367
|
+
await recordDecision(state, paths.taskDir, {
|
|
368
|
+
time: state.lastUpdated,
|
|
369
|
+
step: 'verify',
|
|
370
|
+
fromStatus: previousStatus,
|
|
371
|
+
toStatus: 'active',
|
|
372
|
+
reason: detail?.reason ?? 'verify completed but goal audit still pending',
|
|
373
|
+
evidence: detail?.evidence ?? 'artifacts/验证结果.md',
|
|
374
|
+
});
|
|
375
|
+
await saveWorkflowState(paths.statePath, state);
|
|
376
|
+
}
|
|
377
|
+
export async function markSpecCompleted(repoRoot, taskId) {
|
|
378
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
379
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
380
|
+
const previousStatus = state.status;
|
|
381
|
+
state.sourceState = await computeSourceState(paths.sourceDir, state.sourceState);
|
|
382
|
+
state.status = 'active';
|
|
383
|
+
state.currentStep = 'implement';
|
|
384
|
+
state.lastUpdated = new Date().toISOString();
|
|
385
|
+
state.completedSteps = Array.from(new Set([...state.completedSteps, 'spec', 'analyze', 'plan']));
|
|
386
|
+
state.artifacts.analysis = true;
|
|
387
|
+
state.artifacts.plan = true;
|
|
388
|
+
state.sourceState.plannedHash = state.sourceState.currentHash;
|
|
389
|
+
state.sourceState.lastPlannedAt = state.lastUpdated;
|
|
390
|
+
state.sourceState.lastPlannedStep = 'spec';
|
|
391
|
+
state.sourceState.stalePlan = false;
|
|
392
|
+
await recordDecision(state, paths.taskDir, {
|
|
393
|
+
time: state.lastUpdated,
|
|
394
|
+
step: 'spec',
|
|
395
|
+
fromStatus: previousStatus,
|
|
396
|
+
toStatus: 'active',
|
|
397
|
+
reason: 'spec completed and refreshed plan baseline',
|
|
398
|
+
evidence: 'artifacts/分析报告.md + artifacts/实现计划.md',
|
|
399
|
+
});
|
|
400
|
+
await saveWorkflowState(paths.statePath, state);
|
|
401
|
+
}
|
|
402
|
+
async function updateStepCompletion(repoRoot, taskId, step, completeTask, detail) {
|
|
403
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
404
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
405
|
+
const previousStatus = state.status;
|
|
406
|
+
const nextStep = getNextStep(step);
|
|
407
|
+
state.sourceState = await computeSourceState(paths.sourceDir, state.sourceState);
|
|
408
|
+
state.status = step === 'retrospective' || completeTask ? 'completed' : 'active';
|
|
409
|
+
state.currentStep = nextStep;
|
|
410
|
+
state.lastUpdated = new Date().toISOString();
|
|
411
|
+
state.completedSteps = Array.from(new Set([...state.completedSteps, step]));
|
|
412
|
+
if (step !== 'spec') {
|
|
413
|
+
state.artifacts[ARTIFACT_KEY_BY_STEP[step]] = true;
|
|
414
|
+
}
|
|
415
|
+
if (PLANNING_STEPS.has(step)) {
|
|
416
|
+
state.sourceState.plannedHash = state.sourceState.currentHash;
|
|
417
|
+
state.sourceState.lastPlannedAt = state.lastUpdated;
|
|
418
|
+
state.sourceState.lastPlannedStep = step === 'spec' ? 'spec' : 'plan';
|
|
419
|
+
state.sourceState.stalePlan = false;
|
|
420
|
+
}
|
|
421
|
+
await recordDecision(state, paths.taskDir, {
|
|
422
|
+
time: state.lastUpdated,
|
|
423
|
+
step,
|
|
424
|
+
fromStatus: previousStatus,
|
|
425
|
+
toStatus: state.status,
|
|
426
|
+
reason: detail?.reason ?? `step ${step} completed`,
|
|
427
|
+
evidence: detail?.evidence ?? artifactEvidenceForStep(step),
|
|
428
|
+
});
|
|
429
|
+
await saveWorkflowState(paths.statePath, state);
|
|
430
|
+
if (completeTask) {
|
|
431
|
+
const { cleanupWorktreeAfterTaskCompleted } = await import('./worktree-cleanup.js');
|
|
432
|
+
await cleanupWorktreeAfterTaskCompleted(repoRoot, taskId).catch(() => {
|
|
433
|
+
/* best-effort; errors logged in worktree cleanup */
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
export function resolveTaskFlow(taskConfig) {
|
|
438
|
+
if (taskConfig.flow !== 'auto') {
|
|
439
|
+
return taskConfig.flow;
|
|
440
|
+
}
|
|
441
|
+
if (taskConfig.taskKind === 'feature-study') {
|
|
442
|
+
return 'feature-study';
|
|
443
|
+
}
|
|
444
|
+
return taskConfig.complexity === 'small' ? 'micro' : 'standard';
|
|
445
|
+
}
|
|
446
|
+
export function shouldRunRetrospective(taskConfig) {
|
|
447
|
+
return taskConfig.requireRetrospective || taskConfig.complexity === 'large' || resolveTaskFlow(taskConfig) === 'loop';
|
|
448
|
+
}
|
|
449
|
+
export async function getTaskGoal(repoRoot, taskId) {
|
|
450
|
+
const state = await getTaskStatus(repoRoot, taskId);
|
|
451
|
+
return state.goal;
|
|
452
|
+
}
|
|
453
|
+
export async function setTaskGoal(repoRoot, taskId, objective, options) {
|
|
454
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
455
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
456
|
+
const now = new Date().toISOString();
|
|
457
|
+
const normalizedObjective = objective.trim();
|
|
458
|
+
if (!normalizedObjective) {
|
|
459
|
+
throw new Error('goal objective cannot be empty');
|
|
460
|
+
}
|
|
461
|
+
const nextStatus = options?.status ?? 'active';
|
|
462
|
+
taskGoalStatusSchema.parse(nextStatus);
|
|
463
|
+
state.goal = {
|
|
464
|
+
objective: normalizedObjective,
|
|
465
|
+
status: nextStatus,
|
|
466
|
+
tokenBudget: options?.tokenBudget ?? state.goal.tokenBudget ?? null,
|
|
467
|
+
tokensUsed: state.goal.tokensUsed ?? 0,
|
|
468
|
+
timeUsedSeconds: state.goal.timeUsedSeconds ?? 0,
|
|
469
|
+
pendingObjectiveUpdatedNotice: Boolean(state.goal.objective) && state.goal.objective !== normalizedObjective,
|
|
470
|
+
pendingBudgetLimitNotice: false,
|
|
471
|
+
continuationRuns: 0,
|
|
472
|
+
createdAt: state.goal.createdAt || now,
|
|
473
|
+
updatedAt: now,
|
|
474
|
+
};
|
|
475
|
+
state.lastUpdated = now;
|
|
476
|
+
await saveWorkflowState(paths.statePath, state);
|
|
477
|
+
await appendWorkflowLog(paths.taskDir, `goal set status=${state.goal.status} budget=${state.goal.tokenBudget ?? 'none'} objective=${summarize(state.goal.objective)}`);
|
|
478
|
+
await appendGoalEventRecord(paths.taskDir, {
|
|
479
|
+
event: 'external_set',
|
|
480
|
+
goalStatus: state.goal.status,
|
|
481
|
+
objective: summarize(state.goal.objective),
|
|
482
|
+
at: now,
|
|
483
|
+
details: {
|
|
484
|
+
tokenBudget: state.goal.tokenBudget,
|
|
485
|
+
replacedObjective: Boolean(state.goal.pendingObjectiveUpdatedNotice),
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
return state.goal;
|
|
489
|
+
}
|
|
490
|
+
export async function updateTaskGoalStatus(repoRoot, taskId, status) {
|
|
491
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
492
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
493
|
+
const now = new Date().toISOString();
|
|
494
|
+
taskGoalStatusSchema.parse(status);
|
|
495
|
+
if (!state.goal.objective) {
|
|
496
|
+
throw new Error('goal is not set for this task');
|
|
497
|
+
}
|
|
498
|
+
state.goal.status = status;
|
|
499
|
+
state.goal.updatedAt = now;
|
|
500
|
+
state.lastUpdated = now;
|
|
501
|
+
await saveWorkflowState(paths.statePath, state);
|
|
502
|
+
await appendWorkflowLog(paths.taskDir, `goal status updated to ${status}`);
|
|
503
|
+
await appendGoalEventRecord(paths.taskDir, {
|
|
504
|
+
event: 'status_updated',
|
|
505
|
+
goalStatus: state.goal.status,
|
|
506
|
+
objective: summarize(state.goal.objective),
|
|
507
|
+
at: now,
|
|
508
|
+
});
|
|
509
|
+
return state.goal;
|
|
510
|
+
}
|
|
511
|
+
export async function clearTaskGoal(repoRoot, taskId) {
|
|
512
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
513
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
514
|
+
const now = new Date().toISOString();
|
|
515
|
+
state.goal = {
|
|
516
|
+
objective: '',
|
|
517
|
+
status: 'paused',
|
|
518
|
+
tokenBudget: null,
|
|
519
|
+
tokensUsed: 0,
|
|
520
|
+
timeUsedSeconds: 0,
|
|
521
|
+
pendingObjectiveUpdatedNotice: false,
|
|
522
|
+
pendingBudgetLimitNotice: false,
|
|
523
|
+
continuationRuns: 0,
|
|
524
|
+
createdAt: '',
|
|
525
|
+
updatedAt: now,
|
|
526
|
+
};
|
|
527
|
+
state.lastUpdated = now;
|
|
528
|
+
await saveWorkflowState(paths.statePath, state);
|
|
529
|
+
await appendWorkflowLog(paths.taskDir, 'goal cleared');
|
|
530
|
+
await appendGoalEventRecord(paths.taskDir, {
|
|
531
|
+
event: 'external_clear',
|
|
532
|
+
goalStatus: '',
|
|
533
|
+
objective: '',
|
|
534
|
+
at: now,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
export async function incrementGoalContinuationRuns(repoRoot, taskId) {
|
|
538
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
539
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
540
|
+
if (!state.goal.objective) {
|
|
541
|
+
return state.goal;
|
|
542
|
+
}
|
|
543
|
+
const now = new Date().toISOString();
|
|
544
|
+
state.goal.continuationRuns += 1;
|
|
545
|
+
state.goal.updatedAt = now;
|
|
546
|
+
state.lastUpdated = now;
|
|
547
|
+
await saveWorkflowState(paths.statePath, state);
|
|
548
|
+
await appendWorkflowLog(paths.taskDir, `goal continuation run count=${state.goal.continuationRuns}`);
|
|
549
|
+
return state.goal;
|
|
550
|
+
}
|
|
551
|
+
export async function recordTaskGoalProgress(repoRoot, taskId, usage) {
|
|
552
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
553
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
554
|
+
if (!state.goal.objective) {
|
|
555
|
+
return state.goal;
|
|
556
|
+
}
|
|
557
|
+
const now = new Date().toISOString();
|
|
558
|
+
const tokenDelta = Math.max(0, Math.trunc(usage.tokensUsed ?? 0));
|
|
559
|
+
const timeDelta = Math.max(0, Math.trunc(usage.elapsedSeconds ?? 0));
|
|
560
|
+
state.goal.tokensUsed += tokenDelta;
|
|
561
|
+
state.goal.timeUsedSeconds += timeDelta;
|
|
562
|
+
state.goal.updatedAt = now;
|
|
563
|
+
let budgetLimitedTriggered = false;
|
|
564
|
+
if (state.goal.tokenBudget && state.goal.tokensUsed >= state.goal.tokenBudget && state.goal.status === 'active') {
|
|
565
|
+
state.goal.status = 'budget_limited';
|
|
566
|
+
state.goal.pendingBudgetLimitNotice = true;
|
|
567
|
+
budgetLimitedTriggered = true;
|
|
568
|
+
}
|
|
569
|
+
state.lastUpdated = now;
|
|
570
|
+
await saveWorkflowState(paths.statePath, state);
|
|
571
|
+
if (budgetLimitedTriggered) {
|
|
572
|
+
await appendGoalEventRecord(paths.taskDir, {
|
|
573
|
+
event: 'budget_limited',
|
|
574
|
+
goalStatus: state.goal.status,
|
|
575
|
+
objective: summarize(state.goal.objective),
|
|
576
|
+
at: now,
|
|
577
|
+
details: {
|
|
578
|
+
tokenBudget: state.goal.tokenBudget,
|
|
579
|
+
tokensUsed: state.goal.tokensUsed,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
return state.goal;
|
|
584
|
+
}
|
|
585
|
+
export async function markTaskGoalComplete(repoRoot, taskId, options) {
|
|
586
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
587
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
588
|
+
if (!state.goal.objective)
|
|
589
|
+
return;
|
|
590
|
+
if (!options.completionAuditPassed) {
|
|
591
|
+
throw new Error('cannot mark goal complete without passing goal completion audit');
|
|
592
|
+
}
|
|
593
|
+
const now = new Date().toISOString();
|
|
594
|
+
state.goal.status = 'complete';
|
|
595
|
+
state.goal.pendingObjectiveUpdatedNotice = false;
|
|
596
|
+
state.goal.pendingBudgetLimitNotice = false;
|
|
597
|
+
state.goal.updatedAt = now;
|
|
598
|
+
state.lastUpdated = now;
|
|
599
|
+
await saveWorkflowState(paths.statePath, state);
|
|
600
|
+
await appendWorkflowLog(paths.taskDir, 'goal completed');
|
|
601
|
+
await appendGoalEventRecord(paths.taskDir, {
|
|
602
|
+
event: 'completed',
|
|
603
|
+
goalStatus: state.goal.status,
|
|
604
|
+
objective: summarize(state.goal.objective),
|
|
605
|
+
at: now,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
export async function consumeTaskGoalNotices(repoRoot, taskId) {
|
|
609
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
610
|
+
const state = await loadWorkflowState(paths.statePath);
|
|
611
|
+
const objectiveUpdated = state.goal.pendingObjectiveUpdatedNotice;
|
|
612
|
+
const budgetLimited = state.goal.pendingBudgetLimitNotice;
|
|
613
|
+
if (!objectiveUpdated && !budgetLimited) {
|
|
614
|
+
return { objectiveUpdated: false, budgetLimited: false };
|
|
615
|
+
}
|
|
616
|
+
state.goal.pendingObjectiveUpdatedNotice = false;
|
|
617
|
+
state.goal.pendingBudgetLimitNotice = false;
|
|
618
|
+
state.goal.updatedAt = new Date().toISOString();
|
|
619
|
+
state.lastUpdated = state.goal.updatedAt;
|
|
620
|
+
await saveWorkflowState(paths.statePath, state);
|
|
621
|
+
await appendGoalEventRecord(paths.taskDir, {
|
|
622
|
+
event: 'notices_consumed',
|
|
623
|
+
goalStatus: state.goal.status,
|
|
624
|
+
objective: summarize(state.goal.objective),
|
|
625
|
+
at: state.goal.updatedAt,
|
|
626
|
+
details: {
|
|
627
|
+
objectiveUpdated,
|
|
628
|
+
budgetLimited,
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
return { objectiveUpdated, budgetLimited };
|
|
632
|
+
}
|
|
633
|
+
function artifactEvidenceForStep(step) {
|
|
634
|
+
if (step === 'analyze')
|
|
635
|
+
return 'artifacts/分析报告.md';
|
|
636
|
+
if (step === 'plan')
|
|
637
|
+
return 'artifacts/实现计划.md';
|
|
638
|
+
if (step === 'implement')
|
|
639
|
+
return 'artifacts/修改记录.md';
|
|
640
|
+
if (step === 'verify')
|
|
641
|
+
return 'artifacts/验证结果.md';
|
|
642
|
+
if (step === 'retrospective')
|
|
643
|
+
return 'artifacts/复盘报告.md';
|
|
644
|
+
return '';
|
|
645
|
+
}
|
|
646
|
+
function summarize(text) {
|
|
647
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
648
|
+
}
|
|
649
|
+
async function recordDecision(state, taskDir, decision) {
|
|
650
|
+
state.decisionLog = [...state.decisionLog, decision].slice(-MAX_DECISION_LOG_ENTRIES);
|
|
651
|
+
await appendDecisionRecord(taskDir, decision);
|
|
652
|
+
}
|
|
653
|
+
async function computeSourceState(sourceDir, previous) {
|
|
654
|
+
const files = await listFilesRecursive(sourceDir);
|
|
655
|
+
const relativeFiles = files.map((file) => path.relative(sourceDir, file).replace(/\\/g, '/'));
|
|
656
|
+
const hash = createHash('sha256');
|
|
657
|
+
for (const file of files) {
|
|
658
|
+
const relative = path.relative(sourceDir, file).replace(/\\/g, '/');
|
|
659
|
+
const content = await readFile(file, 'utf-8');
|
|
660
|
+
const normalizedContent = normalizeSourceContentForHash(relative, content);
|
|
661
|
+
hash.update(relative);
|
|
662
|
+
hash.update('\n');
|
|
663
|
+
hash.update(normalizedContent);
|
|
664
|
+
hash.update('\n---\n');
|
|
665
|
+
}
|
|
666
|
+
const currentHash = relativeFiles.length > 0 ? hash.digest('hex') : '';
|
|
667
|
+
const plannedHash = previous?.plannedHash ?? '';
|
|
668
|
+
return {
|
|
669
|
+
currentHash,
|
|
670
|
+
plannedHash,
|
|
671
|
+
trackedFiles: relativeFiles,
|
|
672
|
+
lastCheckedAt: new Date().toISOString(),
|
|
673
|
+
lastPlannedAt: previous?.lastPlannedAt ?? '',
|
|
674
|
+
lastPlannedStep: previous?.lastPlannedStep ?? null,
|
|
675
|
+
stalePlan: Boolean(plannedHash) && plannedHash !== currentHash,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function normalizeSourceContentForHash(relativePath, content) {
|
|
679
|
+
if (relativePath === '需求.md') {
|
|
680
|
+
return content.replace(/-\s*\[[ xX]\]/g, '- [ ]');
|
|
681
|
+
}
|
|
682
|
+
return content;
|
|
683
|
+
}
|
|
684
|
+
async function listFilesRecursive(dir) {
|
|
685
|
+
let entries;
|
|
686
|
+
try {
|
|
687
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
693
|
+
const target = path.join(dir, entry.name);
|
|
694
|
+
if (entry.isDirectory())
|
|
695
|
+
return listFilesRecursive(target);
|
|
696
|
+
return [target];
|
|
697
|
+
}));
|
|
698
|
+
return nested.flat().sort();
|
|
699
|
+
}
|