cc-devflow 4.1.5 → 4.2.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/.claude/CLAUDE.md +87 -1091
- package/.claude/commands/core/architecture.md +32 -2
- package/.claude/commands/core/guidelines.md +27 -2
- package/.claude/commands/core/roadmap.md +33 -4
- package/.claude/commands/core/style.md +53 -263
- package/.claude/commands/flow/CLAUDE.md +28 -0
- package/.claude/commands/flow/archive.md +2 -2
- package/.claude/commands/flow/checklist.md +9 -251
- package/.claude/commands/flow/clarify.md +9 -127
- package/.claude/commands/flow/constitution.md +1 -1
- package/.claude/commands/flow/context.md +1 -1
- package/.claude/commands/flow/dev.md +19 -395
- package/.claude/commands/flow/ideate.md +13 -13
- package/.claude/commands/flow/init.md +19 -30
- package/.claude/commands/flow/new.md +12 -268
- package/.claude/commands/flow/quality.md +10 -153
- package/.claude/commands/flow/release.md +18 -81
- package/.claude/commands/flow/restart.md +15 -16
- package/.claude/commands/flow/spec.md +14 -164
- package/.claude/commands/flow/status.md +12 -12
- package/.claude/commands/flow/update.md +4 -4
- package/.claude/commands/flow/upgrade.md +6 -6
- package/.claude/commands/flow/verify.md +19 -78
- package/.claude/commands/flow/workspace.md +1 -1
- package/.claude/docs/guides/INIT_TROUBLESHOOTING.md +7 -7
- package/.claude/docs/guides/NEW_TROUBLESHOOTING.md +44 -96
- package/.claude/docs/guides/ROADMAP_TROUBLESHOOTING.md +1 -1
- package/.claude/docs/guides/TASK_COMPLETION_MARKING.md +5 -5
- package/.claude/docs/templates/ATTEMPT_TEMPLATE.md +1 -1
- package/.claude/docs/templates/BACKLOG_TEMPLATE.md +3 -3
- package/.claude/docs/templates/CLARIFICATION_REPORT_TEMPLATE.md +5 -5
- package/.claude/docs/templates/ERROR_LOG_TEMPLATE.md +2 -2
- package/.claude/docs/templates/INIT_FLOW_TEMPLATE.md +3 -3
- package/.claude/docs/templates/NEW_ORCHESTRATION_TEMPLATE.md +33 -64
- package/.claude/docs/templates/RESEARCH_TEMPLATE.md +3 -3
- package/.claude/docs/templates/ROADMAP_DIALOGUE_TEMPLATE.md +2 -2
- package/.claude/docs/templates/ROADMAP_TEMPLATE.md +2 -2
- package/.claude/docs/templates/STYLE_TEMPLATE.md +3 -3
- package/.claude/docs/templates/UI_PROTOTYPE_TEMPLATE.md +8 -9
- package/.claude/guides/workflow-guides/flow-orchestrator.md +31 -265
- package/.claude/hooks/CLAUDE.md +1 -1
- package/.claude/hooks/checklist-gate.js +4 -4
- package/.claude/hooks/inject-agent-context.ts +2 -2
- package/.claude/scripts/calculate-checklist-completion.sh +2 -2
- package/.claude/scripts/check-prerequisites.sh +2 -2
- package/.claude/scripts/checklist-errors.sh +4 -4
- package/.claude/scripts/flow-quality-full.sh +5 -5
- package/.claude/scripts/flow-quality-quick.sh +4 -4
- package/.claude/scripts/flow-workspace-init.sh +2 -2
- package/.claude/scripts/generate-clarification-report.sh +4 -4
- package/.claude/scripts/recover-workflow.sh +70 -73
- package/.claude/scripts/run-quality-gates.sh +1 -1
- package/.claude/scripts/setup-epic.sh +2 -2
- package/.claude/scripts/setup-ralph-loop.sh +2 -2
- package/.claude/scripts/validate-research.sh +1 -1
- package/.claude/scripts/verify-setup.sh +1 -1
- package/.claude/skills/cc-devflow-orchestrator/SKILL.md +113 -108
- package/.claude/skills/workflow/CLAUDE.md +24 -0
- package/.claude/skills/workflow/flow-dev/CLAUDE.md +14 -76
- package/.claude/skills/workflow/flow-dev/SKILL.md +58 -60
- package/.claude/skills/workflow/flow-dev/context.jsonl +4 -8
- package/.claude/skills/workflow/flow-init/SKILL.md +46 -144
- package/.claude/skills/workflow/flow-init/assets/RESEARCH_TEMPLATE.md +1 -1
- package/.claude/skills/workflow/flow-init/context.jsonl +3 -3
- package/.claude/skills/workflow/flow-init/scripts/check-prerequisites.sh +1 -1
- package/.claude/skills/workflow/flow-init/scripts/validate-research.sh +1 -1
- package/.claude/skills/workflow/flow-release/SKILL.md +23 -56
- package/.claude/skills/workflow/flow-release/context.jsonl +5 -7
- package/.claude/skills/workflow/flow-spec/CLAUDE.md +15 -101
- package/.claude/skills/workflow/flow-spec/SKILL.md +40 -511
- package/.claude/skills/workflow/flow-spec/context.jsonl +5 -7
- package/.claude/skills/workflow/flow-verify/CLAUDE.md +10 -0
- package/.claude/skills/workflow/flow-verify/SKILL.md +53 -0
- package/.claude/skills/workflow/flow-verify/context.jsonl +5 -0
- package/.claude/skills/workflow.yaml +72 -267
- package/CHANGELOG.md +72 -0
- package/README.md +96 -69
- package/README.zh-CN.md +95 -67
- package/bin/cc-devflow-cli.js +154 -0
- package/bin/harness.js +22 -0
- package/docs/commands/README.md +34 -38
- package/docs/commands/README.zh-CN.md +34 -36
- package/docs/commands/core-roadmap.md +2 -2
- package/docs/commands/core-roadmap.zh-CN.md +2 -2
- package/docs/commands/core-style.md +29 -381
- package/docs/commands/core-style.zh-CN.md +29 -381
- package/docs/commands/flow-init.md +10 -10
- package/docs/commands/flow-init.zh-CN.md +11 -11
- package/docs/commands/flow-new.md +25 -260
- package/docs/commands/flow-new.zh-CN.md +26 -257
- package/docs/guides/getting-started.md +16 -15
- package/docs/guides/getting-started.zh-CN.md +10 -12
- package/lib/compiler/__tests__/manifest.test.js +156 -0
- package/lib/compiler/__tests__/parser.test.js +21 -0
- package/lib/compiler/index.js +17 -1
- package/lib/compiler/manifest.js +68 -6
- package/lib/compiler/parser.js +5 -0
- package/lib/harness/CLAUDE.md +21 -0
- package/lib/harness/cli.js +208 -0
- package/lib/harness/index.js +16 -0
- package/lib/harness/operations/dispatch.js +285 -0
- package/lib/harness/operations/init.js +48 -0
- package/lib/harness/operations/janitor.js +74 -0
- package/lib/harness/operations/pack.js +100 -0
- package/lib/harness/operations/plan.js +29 -0
- package/lib/harness/operations/release.js +83 -0
- package/lib/harness/operations/resume.js +44 -0
- package/lib/harness/operations/verify.js +163 -0
- package/lib/harness/planner.js +141 -0
- package/lib/harness/schemas.js +108 -0
- package/lib/harness/store.js +240 -0
- package/package.json +9 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 manifest/checkpoint/events 与 shell 执行能力,接收并行度与重试参数。
|
|
3
|
+
* [OUTPUT]: 更新 task-manifest 状态,写入每任务 events.jsonl 与 checkpoint.json。
|
|
4
|
+
* [POS]: harness Stage-4 执行入口,被 CLI `harness:dispatch` 与 `harness:resume` 复用。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
nowIso,
|
|
10
|
+
ensureDir,
|
|
11
|
+
appendJsonl,
|
|
12
|
+
writeJson,
|
|
13
|
+
readJson,
|
|
14
|
+
runCommand,
|
|
15
|
+
getTaskManifestPath,
|
|
16
|
+
getRuntimeTaskDir,
|
|
17
|
+
getEventsPath,
|
|
18
|
+
getCheckpointPath
|
|
19
|
+
} = require('../store');
|
|
20
|
+
const { parseManifest, parseCheckpoint } = require('../schemas');
|
|
21
|
+
|
|
22
|
+
function toSessionId(taskId) {
|
|
23
|
+
return `${taskId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dependenciesPassed(task, taskMap) {
|
|
27
|
+
return task.dependsOn.every((depId) => {
|
|
28
|
+
const dep = taskMap.get(depId);
|
|
29
|
+
return dep && dep.status === 'passed';
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function dependenciesFailed(task, taskMap) {
|
|
34
|
+
return task.dependsOn.some((depId) => {
|
|
35
|
+
const dep = taskMap.get(depId);
|
|
36
|
+
return dep && (dep.status === 'failed' || dep.status === 'skipped');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasTouchConflict(task, lockedTouches) {
|
|
41
|
+
if (!task.touches || task.touches.length === 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return task.touches.some((filePath) => lockedTouches.has(filePath));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function selectBatch(readyTasks, parallel) {
|
|
49
|
+
const selected = [];
|
|
50
|
+
const lockedTouches = new Set();
|
|
51
|
+
|
|
52
|
+
for (const task of readyTasks) {
|
|
53
|
+
if (selected.length >= parallel) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (hasTouchConflict(task, lockedTouches)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
selected.push(task);
|
|
62
|
+
for (const touched of task.touches) {
|
|
63
|
+
lockedTouches.add(touched);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return selected;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function writeCheckpoint(repoRoot, changeId, taskId, payload) {
|
|
71
|
+
const checkpointPath = getCheckpointPath(repoRoot, changeId, taskId);
|
|
72
|
+
const checkpoint = parseCheckpoint(payload);
|
|
73
|
+
await writeJson(checkpointPath, checkpoint);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function writeEvent(repoRoot, changeId, taskId, event) {
|
|
77
|
+
const eventsPath = getEventsPath(repoRoot, changeId, taskId);
|
|
78
|
+
await appendJsonl(eventsPath, event);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function executeTask({ repoRoot, changeId, task, retryOverride }) {
|
|
82
|
+
const taskRuntimeDir = getRuntimeTaskDir(repoRoot, changeId, task.id);
|
|
83
|
+
await ensureDir(taskRuntimeDir);
|
|
84
|
+
|
|
85
|
+
const sessionId = toSessionId(task.id);
|
|
86
|
+
const commands = [...task.run, ...task.checks];
|
|
87
|
+
const taskRetry = Number.isInteger(retryOverride) ? retryOverride : task.maxRetries;
|
|
88
|
+
const maxAttempts = Math.max(1, taskRetry + 1);
|
|
89
|
+
|
|
90
|
+
while (task.attempts < maxAttempts) {
|
|
91
|
+
task.attempts += 1;
|
|
92
|
+
task.status = 'running';
|
|
93
|
+
|
|
94
|
+
await writeCheckpoint(repoRoot, changeId, task.id, {
|
|
95
|
+
changeId,
|
|
96
|
+
taskId: task.id,
|
|
97
|
+
sessionId,
|
|
98
|
+
status: task.status,
|
|
99
|
+
summary: `Task started (attempt ${task.attempts}/${maxAttempts})`,
|
|
100
|
+
timestamp: nowIso(),
|
|
101
|
+
attempt: task.attempts
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await writeEvent(repoRoot, changeId, task.id, {
|
|
105
|
+
type: 'task_started',
|
|
106
|
+
changeId,
|
|
107
|
+
taskId: task.id,
|
|
108
|
+
sessionId,
|
|
109
|
+
attempt: task.attempts,
|
|
110
|
+
timestamp: nowIso()
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
let failed = false;
|
|
114
|
+
let failureMessage = '';
|
|
115
|
+
|
|
116
|
+
for (const command of commands) {
|
|
117
|
+
const result = await runCommand(command, {
|
|
118
|
+
cwd: repoRoot,
|
|
119
|
+
timeoutMs: 30 * 60 * 1000
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await writeEvent(repoRoot, changeId, task.id, {
|
|
123
|
+
type: 'command_finished',
|
|
124
|
+
changeId,
|
|
125
|
+
taskId: task.id,
|
|
126
|
+
sessionId,
|
|
127
|
+
attempt: task.attempts,
|
|
128
|
+
command,
|
|
129
|
+
code: result.code,
|
|
130
|
+
durationMs: result.durationMs,
|
|
131
|
+
timestamp: nowIso()
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (result.code !== 0) {
|
|
135
|
+
failed = true;
|
|
136
|
+
failureMessage = (result.stderr || result.stdout || 'Command failed').trim();
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!failed) {
|
|
142
|
+
task.status = 'passed';
|
|
143
|
+
task.lastError = undefined;
|
|
144
|
+
|
|
145
|
+
await writeCheckpoint(repoRoot, changeId, task.id, {
|
|
146
|
+
changeId,
|
|
147
|
+
taskId: task.id,
|
|
148
|
+
sessionId,
|
|
149
|
+
status: task.status,
|
|
150
|
+
summary: `Task passed after ${task.attempts} attempt(s)`,
|
|
151
|
+
timestamp: nowIso(),
|
|
152
|
+
attempt: task.attempts
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await writeEvent(repoRoot, changeId, task.id, {
|
|
156
|
+
type: 'task_passed',
|
|
157
|
+
changeId,
|
|
158
|
+
taskId: task.id,
|
|
159
|
+
sessionId,
|
|
160
|
+
attempts: task.attempts,
|
|
161
|
+
timestamp: nowIso()
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
task.status = 'failed';
|
|
168
|
+
task.lastError = failureMessage;
|
|
169
|
+
|
|
170
|
+
await writeCheckpoint(repoRoot, changeId, task.id, {
|
|
171
|
+
changeId,
|
|
172
|
+
taskId: task.id,
|
|
173
|
+
sessionId,
|
|
174
|
+
status: task.status,
|
|
175
|
+
summary: `Task failed: ${failureMessage.slice(0, 240)}`,
|
|
176
|
+
timestamp: nowIso(),
|
|
177
|
+
attempt: task.attempts
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await writeEvent(repoRoot, changeId, task.id, {
|
|
181
|
+
type: 'task_failed',
|
|
182
|
+
changeId,
|
|
183
|
+
taskId: task.id,
|
|
184
|
+
sessionId,
|
|
185
|
+
attempt: task.attempts,
|
|
186
|
+
error: failureMessage,
|
|
187
|
+
timestamp: nowIso()
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function summarizeTasks(tasks) {
|
|
193
|
+
return tasks.reduce(
|
|
194
|
+
(acc, task) => {
|
|
195
|
+
acc[task.status] = (acc[task.status] || 0) + 1;
|
|
196
|
+
return acc;
|
|
197
|
+
},
|
|
198
|
+
{ pending: 0, running: 0, passed: 0, failed: 0, skipped: 0 }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function runDispatch({ repoRoot, changeId, parallel = 3, maxRetries, resume = false }) {
|
|
203
|
+
const manifestPath = getTaskManifestPath(repoRoot, changeId);
|
|
204
|
+
const manifest = parseManifest(await readJson(manifestPath));
|
|
205
|
+
|
|
206
|
+
if (resume) {
|
|
207
|
+
for (const task of manifest.tasks) {
|
|
208
|
+
if (task.status === 'running') {
|
|
209
|
+
task.status = 'pending';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const safeParallel = Math.max(1, Number.parseInt(parallel, 10) || 1);
|
|
215
|
+
|
|
216
|
+
while (true) {
|
|
217
|
+
const taskMap = new Map(manifest.tasks.map((task) => [task.id, task]));
|
|
218
|
+
const pending = manifest.tasks.filter((task) => task.status === 'pending');
|
|
219
|
+
|
|
220
|
+
if (pending.length === 0) {
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const task of pending) {
|
|
225
|
+
if (dependenciesFailed(task, taskMap)) {
|
|
226
|
+
task.status = 'skipped';
|
|
227
|
+
task.lastError = 'Blocked by failed dependency';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const ready = manifest.tasks.filter(
|
|
232
|
+
(task) => task.status === 'pending' && dependenciesPassed(task, taskMap)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (ready.length === 0) {
|
|
236
|
+
await writeJson(manifestPath, {
|
|
237
|
+
...manifest,
|
|
238
|
+
updatedAt: nowIso()
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
changeId,
|
|
243
|
+
manifestPath,
|
|
244
|
+
summary: summarizeTasks(manifest.tasks),
|
|
245
|
+
success: false,
|
|
246
|
+
reason: 'No ready tasks left. Check dependencies and failed tasks.'
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const batch = selectBatch(ready, safeParallel);
|
|
251
|
+
|
|
252
|
+
if (batch.length === 0) {
|
|
253
|
+
const firstReady = ready[0];
|
|
254
|
+
batch.push(firstReady);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await Promise.all(
|
|
258
|
+
batch.map((task) =>
|
|
259
|
+
executeTask({
|
|
260
|
+
repoRoot,
|
|
261
|
+
changeId,
|
|
262
|
+
task,
|
|
263
|
+
retryOverride: Number.isInteger(maxRetries) ? maxRetries : undefined
|
|
264
|
+
})
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
manifest.updatedAt = nowIso();
|
|
269
|
+
await writeJson(manifestPath, manifest);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const summary = summarizeTasks(manifest.tasks);
|
|
273
|
+
const success = summary.failed === 0 && summary.pending === 0;
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
changeId,
|
|
277
|
+
manifestPath,
|
|
278
|
+
summary,
|
|
279
|
+
success
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
runDispatch
|
|
285
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 store 提供 repo/change 路径与读写能力,接收 changeId/goal 初始化参数。
|
|
3
|
+
* [OUTPUT]: 写入 requirement 目录与 harness-state.json,返回初始化摘要。
|
|
4
|
+
* [POS]: harness Stage-1 初始化入口,被 CLI `harness:init` 调用。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
nowIso,
|
|
10
|
+
ensureDir,
|
|
11
|
+
writeJson,
|
|
12
|
+
readJson,
|
|
13
|
+
getRequirementDir,
|
|
14
|
+
getRuntimeChangeDir,
|
|
15
|
+
getHarnessStatePath
|
|
16
|
+
} = require('../store');
|
|
17
|
+
|
|
18
|
+
async function runInit({ repoRoot, changeId, goal }) {
|
|
19
|
+
const requirementDir = getRequirementDir(repoRoot, changeId);
|
|
20
|
+
const runtimeDir = getRuntimeChangeDir(repoRoot, changeId);
|
|
21
|
+
const statePath = getHarnessStatePath(repoRoot, changeId);
|
|
22
|
+
|
|
23
|
+
await ensureDir(requirementDir);
|
|
24
|
+
await ensureDir(runtimeDir);
|
|
25
|
+
|
|
26
|
+
const previous = (await readJson(statePath, {})) || {};
|
|
27
|
+
const nextState = {
|
|
28
|
+
changeId,
|
|
29
|
+
goal: goal || previous.goal || `Deliver ${changeId} safely with auditable checkpoints.`,
|
|
30
|
+
status: 'initialized',
|
|
31
|
+
initializedAt: previous.initializedAt || nowIso(),
|
|
32
|
+
updatedAt: nowIso()
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
await writeJson(statePath, nextState);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
changeId,
|
|
39
|
+
requirementDir,
|
|
40
|
+
runtimeDir,
|
|
41
|
+
statePath,
|
|
42
|
+
status: nextState.status
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
runInit
|
|
48
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 .harness/runtime 目录与 checkpoint 状态,接收保留小时阈值。
|
|
3
|
+
* [OUTPUT]: 删除过期且非运行中的任务运行态目录,并输出清理统计。
|
|
4
|
+
* [POS]: harness 熵清理入口,被 CLI `harness:janitor` 调用。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const {
|
|
11
|
+
readJson,
|
|
12
|
+
listDirectories,
|
|
13
|
+
getRuntimeRoot
|
|
14
|
+
} = require('../store');
|
|
15
|
+
|
|
16
|
+
async function removeDirectoryRecursive(dirPath) {
|
|
17
|
+
await fs.promises.rm(dirPath, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function isTaskActive(taskDir) {
|
|
21
|
+
const checkpointPath = path.join(taskDir, 'checkpoint.json');
|
|
22
|
+
const checkpoint = await readJson(checkpointPath, null);
|
|
23
|
+
|
|
24
|
+
if (!checkpoint) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return checkpoint.status === 'running';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runJanitor({ repoRoot, hours = 72 }) {
|
|
32
|
+
const runtimeRoot = getRuntimeRoot(repoRoot);
|
|
33
|
+
const cutoffMs = Date.now() - Number(hours) * 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
const changeDirs = await listDirectories(runtimeRoot);
|
|
36
|
+
let removedTaskDirs = 0;
|
|
37
|
+
let removedChangeDirs = 0;
|
|
38
|
+
|
|
39
|
+
for (const changeDir of changeDirs) {
|
|
40
|
+
const taskDirs = await listDirectories(changeDir);
|
|
41
|
+
|
|
42
|
+
for (const taskDir of taskDirs) {
|
|
43
|
+
const stat = await fs.promises.stat(taskDir);
|
|
44
|
+
const isStale = stat.mtimeMs < cutoffMs;
|
|
45
|
+
if (!isStale) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (await isTaskActive(taskDir)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await removeDirectoryRecursive(taskDir);
|
|
54
|
+
removedTaskDirs += 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const remaining = await listDirectories(changeDir);
|
|
58
|
+
if (remaining.length === 0) {
|
|
59
|
+
await removeDirectoryRecursive(changeDir);
|
|
60
|
+
removedChangeDirs += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
runtimeRoot,
|
|
66
|
+
removedTaskDirs,
|
|
67
|
+
removedChangeDirs,
|
|
68
|
+
cutoffHours: Number(hours)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
runJanitor
|
|
74
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 store 读取 harness-state/package scripts,并通过 git 命令收集仓库事实。
|
|
3
|
+
* [OUTPUT]: 生成 context-package.md,提供可复现的目标/约束/下一步命令。
|
|
4
|
+
* [POS]: harness Stage-2 上下文打包入口,被 CLI `harness:pack` 调用。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
nowIso,
|
|
10
|
+
readJson,
|
|
11
|
+
writeText,
|
|
12
|
+
getContextPackagePath,
|
|
13
|
+
getHarnessStatePath,
|
|
14
|
+
runCommand,
|
|
15
|
+
getPackageScripts
|
|
16
|
+
} = require('../store');
|
|
17
|
+
|
|
18
|
+
async function collectGitFacts(repoRoot) {
|
|
19
|
+
const [branch, commit, status] = await Promise.all([
|
|
20
|
+
runCommand('git rev-parse --abbrev-ref HEAD', { cwd: repoRoot }),
|
|
21
|
+
runCommand('git rev-parse HEAD', { cwd: repoRoot }),
|
|
22
|
+
runCommand('git status --short', { cwd: repoRoot })
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
branch: branch.code === 0 ? branch.stdout.trim() : 'unknown',
|
|
27
|
+
commit: commit.code === 0 ? commit.stdout.trim() : 'unknown',
|
|
28
|
+
status: status.code === 0 ? status.stdout.trim() : '(clean)'
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildContextMarkdown({ changeId, goal, gitFacts, scripts }) {
|
|
33
|
+
const nextCommands = [
|
|
34
|
+
`npm run harness:plan -- --change-id ${changeId}`,
|
|
35
|
+
`npm run harness:dispatch -- --change-id ${changeId} --parallel 3`,
|
|
36
|
+
`npm run harness:verify -- --change-id ${changeId} --strict`,
|
|
37
|
+
`npm run harness:release -- --change-id ${changeId}`
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
`# Context Package - ${changeId}`,
|
|
42
|
+
'',
|
|
43
|
+
`- Generated: ${nowIso()}`,
|
|
44
|
+
`- Goal: ${goal}`,
|
|
45
|
+
'',
|
|
46
|
+
'## Repository Facts',
|
|
47
|
+
'',
|
|
48
|
+
`- Branch: ${gitFacts.branch}`,
|
|
49
|
+
`- Commit: ${gitFacts.commit}`,
|
|
50
|
+
'',
|
|
51
|
+
'### Git Status',
|
|
52
|
+
'',
|
|
53
|
+
'```text',
|
|
54
|
+
gitFacts.status || '(clean)',
|
|
55
|
+
'```',
|
|
56
|
+
'',
|
|
57
|
+
'## Constraints',
|
|
58
|
+
'',
|
|
59
|
+
'- Keep tasks dependency-aware and checkpointed.',
|
|
60
|
+
'- Block release when strict verification fails.',
|
|
61
|
+
'- Preserve auditable runtime logs in .harness/runtime/.',
|
|
62
|
+
'',
|
|
63
|
+
'## Available npm scripts',
|
|
64
|
+
'',
|
|
65
|
+
'```text',
|
|
66
|
+
Object.keys(scripts).sort().join('\n') || '(none)',
|
|
67
|
+
'```',
|
|
68
|
+
'',
|
|
69
|
+
'## Next Commands',
|
|
70
|
+
'',
|
|
71
|
+
...nextCommands.map((command) => `- \`${command}\``),
|
|
72
|
+
''
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runPack({ repoRoot, changeId, goal }) {
|
|
77
|
+
const state = (await readJson(getHarnessStatePath(repoRoot, changeId), {})) || {};
|
|
78
|
+
const effectiveGoal = goal || state.goal || `Deliver ${changeId} safely with auditable checkpoints.`;
|
|
79
|
+
const gitFacts = await collectGitFacts(repoRoot);
|
|
80
|
+
const scripts = await getPackageScripts(repoRoot);
|
|
81
|
+
const context = buildContextMarkdown({
|
|
82
|
+
changeId,
|
|
83
|
+
goal: effectiveGoal,
|
|
84
|
+
gitFacts,
|
|
85
|
+
scripts
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const outputPath = getContextPackagePath(repoRoot, changeId);
|
|
89
|
+
await writeText(outputPath, `${context}`);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
changeId,
|
|
93
|
+
outputPath,
|
|
94
|
+
goal: effectiveGoal
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
runPack
|
|
100
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 planner.createTaskManifest,接收 changeId/goal/overwrite 参数。
|
|
3
|
+
* [OUTPUT]: 生成并返回已校验的 task-manifest.json 摘要。
|
|
4
|
+
* [POS]: harness Stage-3 计划生成入口,被 CLI `harness:plan` 调用。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { createTaskManifest } = require('../planner');
|
|
9
|
+
const { getTaskManifestPath } = require('../store');
|
|
10
|
+
|
|
11
|
+
async function runPlan({ repoRoot, changeId, goal, overwrite }) {
|
|
12
|
+
const manifest = await createTaskManifest({
|
|
13
|
+
repoRoot,
|
|
14
|
+
changeId,
|
|
15
|
+
goal,
|
|
16
|
+
overwrite
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
changeId,
|
|
21
|
+
manifestPath: getTaskManifestPath(repoRoot, changeId),
|
|
22
|
+
taskCount: manifest.tasks.length,
|
|
23
|
+
source: manifest.metadata.source
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
runPlan
|
|
29
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 report-card 与 task-manifest 的最终状态。
|
|
3
|
+
* [OUTPUT]: 在 requirement 目录生成 RELEASE_NOTE.md,并更新 harness-state 为 released。
|
|
4
|
+
* [POS]: harness 发布收尾入口,被 CLI `harness:release` 调用。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
nowIso,
|
|
10
|
+
readJson,
|
|
11
|
+
writeText,
|
|
12
|
+
writeJson,
|
|
13
|
+
getReportCardPath,
|
|
14
|
+
getTaskManifestPath,
|
|
15
|
+
getReleaseNotePath,
|
|
16
|
+
getHarnessStatePath
|
|
17
|
+
} = require('../store');
|
|
18
|
+
const { parseReportCard, parseManifest } = require('../schemas');
|
|
19
|
+
|
|
20
|
+
function formatReleaseNote({ changeId, manifest, report }) {
|
|
21
|
+
const passedTasks = manifest.tasks.filter((task) => task.status === 'passed');
|
|
22
|
+
const failedTasks = manifest.tasks.filter((task) => task.status === 'failed');
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
`# Release Note - ${changeId}`,
|
|
26
|
+
'',
|
|
27
|
+
`- Released At: ${nowIso()}`,
|
|
28
|
+
`- Verification: ${report.overall.toUpperCase()}`,
|
|
29
|
+
'',
|
|
30
|
+
'## Task Summary',
|
|
31
|
+
'',
|
|
32
|
+
`- Passed: ${passedTasks.length}`,
|
|
33
|
+
`- Failed: ${failedTasks.length}`,
|
|
34
|
+
'',
|
|
35
|
+
'## Completed Tasks',
|
|
36
|
+
'',
|
|
37
|
+
...(passedTasks.length > 0
|
|
38
|
+
? passedTasks.map((task) => `- ${task.id}: ${task.title}`)
|
|
39
|
+
: ['- (none)']),
|
|
40
|
+
'',
|
|
41
|
+
'## Blocking Findings',
|
|
42
|
+
'',
|
|
43
|
+
...(report.blockingFindings.length > 0
|
|
44
|
+
? report.blockingFindings.map((item) => `- ${item}`)
|
|
45
|
+
: ['- None']),
|
|
46
|
+
''
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runRelease({ repoRoot, changeId }) {
|
|
51
|
+
const reportPath = getReportCardPath(repoRoot, changeId);
|
|
52
|
+
const manifestPath = getTaskManifestPath(repoRoot, changeId);
|
|
53
|
+
const statePath = getHarnessStatePath(repoRoot, changeId);
|
|
54
|
+
|
|
55
|
+
const report = parseReportCard(await readJson(reportPath));
|
|
56
|
+
const manifest = parseManifest(await readJson(manifestPath));
|
|
57
|
+
|
|
58
|
+
if (report.overall !== 'pass') {
|
|
59
|
+
throw new Error('Release blocked: report-card overall is not pass');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const note = formatReleaseNote({ changeId, manifest, report });
|
|
63
|
+
const releaseNotePath = getReleaseNotePath(repoRoot, changeId);
|
|
64
|
+
|
|
65
|
+
await writeText(releaseNotePath, note);
|
|
66
|
+
await writeJson(statePath, {
|
|
67
|
+
changeId,
|
|
68
|
+
goal: manifest.goal,
|
|
69
|
+
status: 'released',
|
|
70
|
+
releasedAt: nowIso(),
|
|
71
|
+
updatedAt: nowIso()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
changeId,
|
|
76
|
+
releaseNotePath,
|
|
77
|
+
status: 'released'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
runRelease
|
|
83
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 manifest 当前任务状态与 dispatch 执行器,接收 changeId/并行度/重试参数。
|
|
3
|
+
* [OUTPUT]: 将 running 与可重试 failed 任务回置为 pending,并继续调度执行。
|
|
4
|
+
* [POS]: harness 恢复执行入口,被 CLI `harness:resume` 调用。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { readJson, writeJson, getTaskManifestPath } = require('../store');
|
|
9
|
+
const { parseManifest } = require('../schemas');
|
|
10
|
+
const { runDispatch } = require('./dispatch');
|
|
11
|
+
|
|
12
|
+
async function runResume({ repoRoot, changeId, parallel, maxRetries }) {
|
|
13
|
+
const manifestPath = getTaskManifestPath(repoRoot, changeId);
|
|
14
|
+
const manifest = parseManifest(await readJson(manifestPath));
|
|
15
|
+
|
|
16
|
+
for (const task of manifest.tasks) {
|
|
17
|
+
if (task.status === 'running') {
|
|
18
|
+
task.status = 'pending';
|
|
19
|
+
task.lastError = 'Resumed from interrupted running state';
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (task.status === 'failed') {
|
|
24
|
+
const retryLimit = Number.isInteger(maxRetries) ? maxRetries : task.maxRetries;
|
|
25
|
+
if (task.attempts <= retryLimit) {
|
|
26
|
+
task.status = 'pending';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await writeJson(manifestPath, manifest);
|
|
32
|
+
|
|
33
|
+
return runDispatch({
|
|
34
|
+
repoRoot,
|
|
35
|
+
changeId,
|
|
36
|
+
parallel,
|
|
37
|
+
maxRetries,
|
|
38
|
+
resume: true
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
runResume
|
|
44
|
+
};
|