@tea-agent/loop-agent 0.3.0 → 0.5.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 +16 -14
- package/CHANGELOG.md +70 -53
- package/README.md +28 -25
- package/bin/agent-worker.js +22 -0
- package/dist/application/dag/validate-dag.js +14 -1
- package/dist/commands/init.js +220 -32
- package/dist/executors/config-core.js +3 -2
- package/dist/executors/dag-pi-executor.js +8 -1
- package/dist/executors/model-routing.js +43 -0
- package/dist/governance/manifest-types.js +9 -1
- package/dist/worker/cli.js +119 -0
- package/dist/worker/loop-agent/command-result.js +1 -0
- package/dist/worker/loop-agent/loop-agent-client.js +105 -0
- package/dist/worker/loop-agent/parse-json.js +14 -0
- package/dist/worker/materialize/harness-task-materializer.js +157 -0
- package/dist/worker/pool/failure-routing.js +98 -0
- package/dist/worker/pool/run-store.js +125 -0
- package/dist/worker/pool/types.js +1 -0
- package/dist/worker/preflight.js +108 -0
- package/dist/worker/profile-mapping.js +76 -0
- package/dist/worker/progress-reporter.js +81 -0
- package/dist/worker/report/morning-report.js +69 -0
- package/dist/worker/repos/repo-resolver.js +23 -0
- package/dist/worker/run-task/run-task.js +359 -0
- package/dist/worker/runner/run-ready.js +216 -0
- package/dist/worker/task-graph/acceptance-schema.js +25 -0
- package/dist/worker/task-graph/ready-queue.js +23 -0
- package/dist/worker/task-graph/task-graph-schema.js +28 -0
- package/dist/worker/task-graph/types.js +1 -0
- package/dist/worker/task-graph/validate.js +188 -0
- package/dist/worker/task-spec/complexity-mapping.js +8 -0
- package/dist/worker/task-spec/schema.js +116 -0
- package/dist/worker/task-spec/types.js +1 -0
- package/dist/worker/task-spec/validate.js +352 -0
- package/dist/workflows/dag/init-hybrid.js +4 -13
- package/dist/workflows/dag/skill-instructions.js +4 -0
- package/dist/workflows/dag/types.js +1 -1
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/README.md +11 -7
- package/docs/development-principles.md +2 -0
- package/docs/exec-plans/active/README.md +1 -1
- package/docs/exec-plans/completed/README.md +8 -0
- package/docs/init-surface.manifest.json +199 -175
- package/docs/skills/vetted-skill-registry.md +4 -4
- package/docs/templates/agent-dag.base.json +1 -1
- package/docs/templates/agent-dag.final-verification.json +1 -1
- package/docs/templates/agent-dag.supervised-implementation.json +1 -1
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/init-evolution-review.md +33 -33
- package/examples/example-dag.json +1 -1
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/harness.json +7 -32
- package/package.json +14 -12
- package/skills/init-capability-evolution/SKILL.md +69 -69
- package/skills/loop-agent/SKILL.md +2 -0
- package/skills/loop-agent/references/command-reference.md +63 -35
- package/skills/loop-agent/references/harness-policy.md +2 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { resolveLoopAgentProfile } from "../profile-mapping.js";
|
|
5
|
+
import { mapRiskLevelToComplexity } from "./complexity-mapping.js";
|
|
6
|
+
import { taskSpecSchema } from "./schema.js";
|
|
7
|
+
const CODE_WRITING_TYPES = new Set([
|
|
8
|
+
"backend-feature",
|
|
9
|
+
"frontend-feature",
|
|
10
|
+
"bugfix",
|
|
11
|
+
"qa-testcode",
|
|
12
|
+
]);
|
|
13
|
+
const VERIFY_OPTIONAL_TYPES = new Set(["qa-analysis", "reviewer-gate"]);
|
|
14
|
+
const QA_TYPES = new Set([
|
|
15
|
+
"qa-analysis",
|
|
16
|
+
"qa-casegen",
|
|
17
|
+
"qa-testcode",
|
|
18
|
+
"qa-execute",
|
|
19
|
+
"qa-failure-analysis",
|
|
20
|
+
]);
|
|
21
|
+
export async function validateTaskSpec(input, options = {}) {
|
|
22
|
+
const schemaResult = taskSpecSchema.safeParse(input);
|
|
23
|
+
const fallback = fallbackIdentity(input);
|
|
24
|
+
if (!schemaResult.success) {
|
|
25
|
+
const errors = schemaResult.error.issues.map((issue) => ({
|
|
26
|
+
layer: "schema",
|
|
27
|
+
code: "schema-invalid",
|
|
28
|
+
message: issue.message,
|
|
29
|
+
path: issue.path.join("."),
|
|
30
|
+
}));
|
|
31
|
+
const failure = firstFailure(errors, "schema-invalid", "TaskSpec schema validation failed");
|
|
32
|
+
return buildResult({
|
|
33
|
+
taskId: fallback.taskId,
|
|
34
|
+
featureId: fallback.featureId,
|
|
35
|
+
businessProfile: fallback.businessProfile,
|
|
36
|
+
riskLevel: fallback.riskLevel,
|
|
37
|
+
checks: {
|
|
38
|
+
schema: failure,
|
|
39
|
+
sourceDocs: failureFor("schema-invalid", "Skipped because schema validation failed"),
|
|
40
|
+
acceptanceRefs: failureFor("schema-invalid", "Skipped because schema validation failed"),
|
|
41
|
+
dependsOn: "skipped",
|
|
42
|
+
pathBoundary: failureFor("schema-invalid", "Skipped because schema validation failed"),
|
|
43
|
+
verifyCommands: failureFor("schema-invalid", "Skipped because schema validation failed"),
|
|
44
|
+
profileMapping: failureFor("schema-invalid", "Skipped because schema validation failed"),
|
|
45
|
+
},
|
|
46
|
+
errors,
|
|
47
|
+
warnings: [],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const taskSpec = schemaResult.data;
|
|
51
|
+
const errors = [];
|
|
52
|
+
const warnings = [];
|
|
53
|
+
const sourceDocsCheck = await checkSourceDocs(taskSpec, options, errors);
|
|
54
|
+
const acceptanceRefsCheck = await checkAcceptanceRefs(taskSpec, options, errors);
|
|
55
|
+
const dependsOnCheck = await checkDependsOn(taskSpec, options, errors);
|
|
56
|
+
const pathBoundaryCheck = checkPathBoundaries(taskSpec, errors, warnings);
|
|
57
|
+
const verifyCommandsCheck = checkVerifyCommands(taskSpec, errors);
|
|
58
|
+
const profileMapping = resolveLoopAgentProfile(taskSpec);
|
|
59
|
+
return buildResult({
|
|
60
|
+
taskId: taskSpec.id,
|
|
61
|
+
featureId: taskSpec.feature_id,
|
|
62
|
+
businessProfile: taskSpec.type,
|
|
63
|
+
riskLevel: taskSpec.risk_level,
|
|
64
|
+
loopAgentProfilePreview: profileMapping.loopAgentProfile,
|
|
65
|
+
checks: {
|
|
66
|
+
schema: "ok",
|
|
67
|
+
sourceDocs: sourceDocsCheck,
|
|
68
|
+
acceptanceRefs: acceptanceRefsCheck,
|
|
69
|
+
dependsOn: dependsOnCheck,
|
|
70
|
+
pathBoundary: pathBoundaryCheck,
|
|
71
|
+
verifyCommands: verifyCommandsCheck,
|
|
72
|
+
profileMapping: "ok",
|
|
73
|
+
},
|
|
74
|
+
errors,
|
|
75
|
+
warnings,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function buildResult(input) {
|
|
79
|
+
const riskLevel = input.riskLevel === "low" ||
|
|
80
|
+
input.riskLevel === "medium" ||
|
|
81
|
+
input.riskLevel === "high"
|
|
82
|
+
? input.riskLevel
|
|
83
|
+
: "medium";
|
|
84
|
+
return {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
ok: input.errors.length === 0 && checksAreOk(input.checks),
|
|
87
|
+
taskId: input.taskId,
|
|
88
|
+
featureId: input.featureId,
|
|
89
|
+
businessProfile: input.businessProfile,
|
|
90
|
+
riskLevel: input.riskLevel,
|
|
91
|
+
complexityPreview: mapRiskLevelToComplexity(riskLevel),
|
|
92
|
+
loopAgentProfilePreview: input.loopAgentProfilePreview,
|
|
93
|
+
checks: input.checks,
|
|
94
|
+
errors: input.errors,
|
|
95
|
+
warnings: input.warnings,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function checksAreOk(checks) {
|
|
99
|
+
return Object.values(checks).every((check) => check === "ok" || check === "skipped");
|
|
100
|
+
}
|
|
101
|
+
async function checkSourceDocs(taskSpec, options, errors) {
|
|
102
|
+
const baseDir = getTaskSpecDir(options);
|
|
103
|
+
if (!baseDir)
|
|
104
|
+
return "ok";
|
|
105
|
+
for (const [key, relativePath] of Object.entries(taskSpec.source_docs)) {
|
|
106
|
+
if (!relativePath)
|
|
107
|
+
continue;
|
|
108
|
+
if (!(await exists(path.resolve(baseDir, relativePath)))) {
|
|
109
|
+
const error = {
|
|
110
|
+
layer: "file-existence",
|
|
111
|
+
code: "source-doc-missing",
|
|
112
|
+
message: `source_docs.${key} does not exist: ${relativePath}`,
|
|
113
|
+
path: `source_docs.${key}`,
|
|
114
|
+
};
|
|
115
|
+
errors.push(error);
|
|
116
|
+
return toFailure(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return "ok";
|
|
120
|
+
}
|
|
121
|
+
async function checkAcceptanceRefs(taskSpec, options, errors) {
|
|
122
|
+
const baseDir = getTaskSpecDir(options);
|
|
123
|
+
if (!baseDir)
|
|
124
|
+
return "ok";
|
|
125
|
+
const acceptancePath = path.resolve(baseDir, taskSpec.source_docs.acceptance);
|
|
126
|
+
if (!(await exists(acceptancePath))) {
|
|
127
|
+
const error = {
|
|
128
|
+
layer: "file-existence",
|
|
129
|
+
code: "acceptance-source-missing",
|
|
130
|
+
message: `acceptance source does not exist: ${taskSpec.source_docs.acceptance}`,
|
|
131
|
+
path: "source_docs.acceptance",
|
|
132
|
+
};
|
|
133
|
+
errors.push(error);
|
|
134
|
+
return toFailure(error);
|
|
135
|
+
}
|
|
136
|
+
const parsed = YAML.parse(await readFile(acceptancePath, "utf-8"));
|
|
137
|
+
const ids = collectStringIds(parsed);
|
|
138
|
+
for (const acceptanceRef of taskSpec.acceptance_refs) {
|
|
139
|
+
if (!ids.has(acceptanceRef)) {
|
|
140
|
+
const error = {
|
|
141
|
+
layer: "file-existence",
|
|
142
|
+
code: "acceptance-ref-missing",
|
|
143
|
+
message: `acceptance ref not found in acceptance source: ${acceptanceRef}`,
|
|
144
|
+
path: "acceptance_refs",
|
|
145
|
+
};
|
|
146
|
+
errors.push(error);
|
|
147
|
+
return toFailure(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return "ok";
|
|
151
|
+
}
|
|
152
|
+
async function checkDependsOn(taskSpec, options, errors) {
|
|
153
|
+
if (taskSpec.depends_on.length === 0)
|
|
154
|
+
return "skipped";
|
|
155
|
+
const baseDir = getTaskSpecDir(options);
|
|
156
|
+
if (!baseDir)
|
|
157
|
+
return "ok";
|
|
158
|
+
for (const dependency of taskSpec.depends_on) {
|
|
159
|
+
const dependencyPath = path.resolve(baseDir, `${dependency}.yaml`);
|
|
160
|
+
if (!(await exists(dependencyPath))) {
|
|
161
|
+
const error = {
|
|
162
|
+
layer: "file-existence",
|
|
163
|
+
code: "depends-on-missing",
|
|
164
|
+
message: `depends_on task file does not exist: ${dependency}.yaml`,
|
|
165
|
+
path: "depends_on",
|
|
166
|
+
};
|
|
167
|
+
errors.push(error);
|
|
168
|
+
return toFailure(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return "ok";
|
|
172
|
+
}
|
|
173
|
+
function checkPathBoundaries(taskSpec, errors, warnings) {
|
|
174
|
+
const start = errors.length;
|
|
175
|
+
for (const [index, allowedPath] of taskSpec.constraints.allowed_paths.entries()) {
|
|
176
|
+
if (isBroadAllowedPath(allowedPath)) {
|
|
177
|
+
errors.push({
|
|
178
|
+
layer: "path-boundary",
|
|
179
|
+
code: "broad-allowed-paths",
|
|
180
|
+
message: `allowed_paths entry is too broad: ${allowedPath}`,
|
|
181
|
+
path: `constraints.allowed_paths[${index}]`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
for (const [forbiddenIndex, forbiddenPath] of taskSpec.constraints.forbidden_paths.entries()) {
|
|
185
|
+
if (patternsOverlap(allowedPath, forbiddenPath)) {
|
|
186
|
+
errors.push({
|
|
187
|
+
layer: "path-boundary",
|
|
188
|
+
code: "allowed-forbidden-overlap",
|
|
189
|
+
message: `allowed path overlaps forbidden path: ${allowedPath} / ${forbiddenPath}`,
|
|
190
|
+
path: `constraints.forbidden_paths[${forbiddenIndex}]`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (CODE_WRITING_TYPES.has(taskSpec.type) &&
|
|
196
|
+
taskSpec.constraints.allowed_paths.length === 0) {
|
|
197
|
+
errors.push({
|
|
198
|
+
layer: "path-boundary",
|
|
199
|
+
code: "code-task-empty-allowed-paths",
|
|
200
|
+
message: `${taskSpec.type} requires non-empty allowed_paths`,
|
|
201
|
+
path: "constraints.allowed_paths",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (taskSpec.constraints.forbidden_paths.length === 0) {
|
|
205
|
+
errors.push({
|
|
206
|
+
layer: "path-boundary",
|
|
207
|
+
code: "forbidden-paths-empty",
|
|
208
|
+
message: "TaskSpec requires non-empty forbidden_paths",
|
|
209
|
+
path: "constraints.forbidden_paths",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (QA_TYPES.has(taskSpec.type) && !hasQaBusinessCodeForbiddenPath(taskSpec)) {
|
|
213
|
+
errors.push({
|
|
214
|
+
layer: "path-boundary",
|
|
215
|
+
code: "qa-business-code-not-forbidden",
|
|
216
|
+
message: "QA tasks must forbid common business code directories",
|
|
217
|
+
path: "constraints.forbidden_paths",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const complexity = mapRiskLevelToComplexity(taskSpec.risk_level);
|
|
221
|
+
if ((complexity === "medium" || complexity === "large") &&
|
|
222
|
+
taskSpec.constraints.allowed_paths.length === 0) {
|
|
223
|
+
errors.push({
|
|
224
|
+
layer: "path-boundary",
|
|
225
|
+
code: "empty-allowed-paths-for-complexity",
|
|
226
|
+
message: `${complexity} tasks require non-empty allowed_paths before materialize`,
|
|
227
|
+
path: "constraints.allowed_paths",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (complexity === "small" && taskSpec.constraints.allowed_paths.length === 0) {
|
|
231
|
+
warnings.push({
|
|
232
|
+
code: "empty-allowed-paths-path-guard-risk",
|
|
233
|
+
message: "empty allowed_paths will make non-harness writes fail path guard",
|
|
234
|
+
path: "constraints.allowed_paths",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const pathErrors = errors.slice(start).filter((error) => error.layer === "path-boundary");
|
|
238
|
+
if (pathErrors.length === 0)
|
|
239
|
+
return "ok";
|
|
240
|
+
return toFailure(pathErrors[0]);
|
|
241
|
+
}
|
|
242
|
+
function checkVerifyCommands(taskSpec, errors) {
|
|
243
|
+
if (!VERIFY_OPTIONAL_TYPES.has(taskSpec.type) &&
|
|
244
|
+
taskSpec.verify.commands.length === 0) {
|
|
245
|
+
const error = {
|
|
246
|
+
layer: "file-existence",
|
|
247
|
+
code: "verify-commands-empty",
|
|
248
|
+
message: `${taskSpec.type} requires verify.commands`,
|
|
249
|
+
path: "verify.commands",
|
|
250
|
+
};
|
|
251
|
+
errors.push(error);
|
|
252
|
+
return toFailure(error);
|
|
253
|
+
}
|
|
254
|
+
return "ok";
|
|
255
|
+
}
|
|
256
|
+
function getTaskSpecDir(options) {
|
|
257
|
+
return options.taskSpecPath ? path.dirname(options.taskSpecPath) : undefined;
|
|
258
|
+
}
|
|
259
|
+
async function exists(filePath) {
|
|
260
|
+
try {
|
|
261
|
+
await access(filePath);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function collectStringIds(value, ids = new Set()) {
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
for (const item of value)
|
|
271
|
+
collectStringIds(item, ids);
|
|
272
|
+
return ids;
|
|
273
|
+
}
|
|
274
|
+
if (value && typeof value === "object") {
|
|
275
|
+
for (const [key, child] of Object.entries(value)) {
|
|
276
|
+
if (key === "id" && typeof child === "string")
|
|
277
|
+
ids.add(child);
|
|
278
|
+
collectStringIds(child, ids);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return ids;
|
|
282
|
+
}
|
|
283
|
+
function isBroadAllowedPath(pattern) {
|
|
284
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
285
|
+
return normalized === "**" || normalized === "./**" || normalized === ".";
|
|
286
|
+
}
|
|
287
|
+
function probeForPattern(pattern) {
|
|
288
|
+
return pattern
|
|
289
|
+
.replace(/\\/g, "/")
|
|
290
|
+
.replace(/\/\*\*$/, "/_probe_")
|
|
291
|
+
.replace(/\*\*$/, "_probe_")
|
|
292
|
+
.replace(/\*/g, "_")
|
|
293
|
+
.replace(/^\.\//, "");
|
|
294
|
+
}
|
|
295
|
+
function patternsOverlap(left, right) {
|
|
296
|
+
const leftProbe = probeForPattern(left);
|
|
297
|
+
const rightProbe = probeForPattern(right);
|
|
298
|
+
return (pathMatchesPattern(leftProbe, right) ||
|
|
299
|
+
pathMatchesPattern(rightProbe, left) ||
|
|
300
|
+
leftProbe === rightProbe);
|
|
301
|
+
}
|
|
302
|
+
function pathMatchesPattern(filePath, pattern) {
|
|
303
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
304
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
305
|
+
if (normalizedPattern.endsWith("/**")) {
|
|
306
|
+
const prefix = normalizedPattern.slice(0, -3);
|
|
307
|
+
return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
|
|
308
|
+
}
|
|
309
|
+
if (normalizedPattern.includes("*")) {
|
|
310
|
+
const escaped = normalizedPattern
|
|
311
|
+
.split("*")
|
|
312
|
+
.map((part) => part.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"))
|
|
313
|
+
.join(".*");
|
|
314
|
+
return new RegExp(`^${escaped}$`).test(normalizedPath);
|
|
315
|
+
}
|
|
316
|
+
return normalizedPath === normalizedPattern;
|
|
317
|
+
}
|
|
318
|
+
function hasQaBusinessCodeForbiddenPath(taskSpec) {
|
|
319
|
+
return taskSpec.constraints.forbidden_paths.some((forbiddenPath) => {
|
|
320
|
+
const normalized = forbiddenPath.replace(/\\/g, "/");
|
|
321
|
+
return (normalized.includes("/src/") ||
|
|
322
|
+
normalized.includes("src/**") ||
|
|
323
|
+
normalized.includes("/services/") ||
|
|
324
|
+
normalized.includes("services/**") ||
|
|
325
|
+
normalized.includes("/app/") ||
|
|
326
|
+
normalized.includes("app/**") ||
|
|
327
|
+
normalized.includes("/apps/"));
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function fallbackIdentity(input) {
|
|
331
|
+
const record = input && typeof input === "object" ? input : {};
|
|
332
|
+
return {
|
|
333
|
+
taskId: typeof record.id === "string" ? record.id : "",
|
|
334
|
+
featureId: typeof record.feature_id === "string" ? record.feature_id : "",
|
|
335
|
+
businessProfile: typeof record.type === "string" ? record.type : "",
|
|
336
|
+
riskLevel: typeof record.risk_level === "string" ? record.risk_level : "medium",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function failureFor(code, message, pathValue) {
|
|
340
|
+
return { status: "failed", code, message, path: pathValue };
|
|
341
|
+
}
|
|
342
|
+
function firstFailure(errors, code, message) {
|
|
343
|
+
return errors[0] ? toFailure(errors[0]) : failureFor(code, message);
|
|
344
|
+
}
|
|
345
|
+
function toFailure(error) {
|
|
346
|
+
return {
|
|
347
|
+
status: "failed",
|
|
348
|
+
code: error.code,
|
|
349
|
+
message: error.message,
|
|
350
|
+
path: error.path,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -2,7 +2,7 @@ import { access, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertValidDagSpec } from "./validate.js";
|
|
5
|
-
import { parseDagSpec, } from "./types.js";
|
|
5
|
+
import { DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
7
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
8
8
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
@@ -10,6 +10,7 @@ import { loadHarnessManifest } from "../../governance/harness.js";
|
|
|
10
10
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
11
11
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
12
12
|
import { resolveVerifyPreset } from "../../executors/shell-verification.js";
|
|
13
|
+
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
13
14
|
const REQUIREMENT_FILE = "需求.md";
|
|
14
15
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
15
16
|
const MAX_SOURCE_EXCERPT_CHARS = 2000;
|
|
@@ -301,6 +302,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
301
302
|
constraintMarkdown,
|
|
302
303
|
taskConfig,
|
|
303
304
|
enabledExecutors: resolveEnabledExecutors(manifest.executors),
|
|
305
|
+
executorModelMatrix: resolveExecutorModelMatrices(manifest),
|
|
304
306
|
verifyCommands,
|
|
305
307
|
};
|
|
306
308
|
}
|
|
@@ -336,18 +338,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
336
338
|
contextProfile: taskConfig.contextProfile,
|
|
337
339
|
},
|
|
338
340
|
skillsByRole: HYBRID_SKILLS_BY_ROLE,
|
|
339
|
-
executorModels:
|
|
340
|
-
cursor: {
|
|
341
|
-
LOW: "composer-2.5",
|
|
342
|
-
MED: "composer-2.5",
|
|
343
|
-
HIGH: "composer-2.5",
|
|
344
|
-
},
|
|
345
|
-
pi: {
|
|
346
|
-
LOW: "gpt-5.3-codex-spark",
|
|
347
|
-
MED: "glm-5.2",
|
|
348
|
-
HIGH: "gpt-5.5",
|
|
349
|
-
},
|
|
350
|
-
},
|
|
341
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
351
342
|
tasks: [
|
|
352
343
|
{
|
|
353
344
|
id: "contract-pi",
|
|
@@ -42,6 +42,10 @@ function buildSkillCandidatePaths(input) {
|
|
|
42
42
|
const piSkillsDir = path.join(input.homeDir, ".pi", "agent", "skills");
|
|
43
43
|
candidates.push(normalizeCandidate(path.join(piSkillsDir, input.name, "SKILL.md")), normalizeCandidate(path.join(piSkillsDir, `${input.name}.md`)));
|
|
44
44
|
candidates.push(normalizeCandidate(path.join(input.cwd, "skills", input.name, "SKILL.md")));
|
|
45
|
+
// Agent-compatible project path (e.g. OpenCode). `skills/` stays the primary
|
|
46
|
+
// loop-agent repo-local path; `.agents/skills` is searched after it and before
|
|
47
|
+
// bundled skills so a project with only the mirror can still resolve.
|
|
48
|
+
candidates.push(normalizeCandidate(path.join(input.cwd, ".agents", "skills", input.name, "SKILL.md")));
|
|
45
49
|
if (input.name === "loop-agent") {
|
|
46
50
|
candidates.push(normalizeCandidate(path.join(input.cwd, "skill", "SKILL.md")));
|
|
47
51
|
}
|
|
@@ -428,7 +428,7 @@ function validateDecisionGateTaskConfig(task, issues) {
|
|
|
428
428
|
});
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
|
-
export function collectExecutorModelWarnings(spec) {
|
|
431
|
+
export function collectExecutorModelWarnings(spec, expectedMatrices = DEFAULT_DAG_EXECUTOR_MODELS) {
|
|
432
432
|
const warnings = [];
|
|
433
433
|
const executors = Object.keys(DEFAULT_DAG_EXECUTOR_MODELS);
|
|
434
434
|
for (const executor of executors) {
|
|
@@ -436,11 +436,12 @@ export function collectExecutorModelWarnings(spec) {
|
|
|
436
436
|
if (!actualByComplexity) {
|
|
437
437
|
continue;
|
|
438
438
|
}
|
|
439
|
+
const expectedByComplexity = expectedMatrices[executor] ?? DEFAULT_DAG_EXECUTOR_MODELS[executor];
|
|
439
440
|
for (const [complexityKey, actual] of Object.entries(actualByComplexity)) {
|
|
440
441
|
if (actual === undefined) {
|
|
441
442
|
continue;
|
|
442
443
|
}
|
|
443
|
-
const expected =
|
|
444
|
+
const expected = expectedByComplexity[complexityKey];
|
|
444
445
|
if (actual !== expected) {
|
|
445
446
|
warnings.push({
|
|
446
447
|
executor,
|
package/docs/README.md
CHANGED
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
- `loop-agent-harness.md` — runtime 与 command surface 概览
|
|
15
15
|
- `agent-dag-runner.md` — Agent DAG runner 指南
|
|
16
16
|
- `cursor-executor-usage.md` — Cursor executor 用法
|
|
17
|
-
- `dynamic-workflow-dag-engine-roadmap.md` — Dynamic Workflow DAG Engine 路线图与适配分析
|
|
18
|
-
- `init-surface.manifest.json` — npm 包范围、目标项目初始化投影与 `init check-update` surface 分类的机器校验契约
|
|
17
|
+
- `dynamic-workflow-dag-engine-roadmap.md` — Dynamic Workflow DAG Engine 路线图与适配分析
|
|
18
|
+
- `init-surface.manifest.json` — npm 包范围、目标项目初始化投影与 `init check-update` surface 分类的机器校验契约
|
|
19
|
+
|
|
20
|
+
## 设计思想来源
|
|
21
|
+
|
|
22
|
+
- `../website/docs/practices/` — Anthropic 长时运行 agent harness、OpenAI Codex harness engineering、腾讯端到端 Harness Engineering 与社区 agent harness 实践资料。当前仓库的“人类掌舵、智能体执行”、仓库即记录系统、小步增量、结构化 handoff 和 shell verification 纪律均受这些实践启发;权威执行规则仍以本目录治理文档、根目录 AGENTS.md、harness.json、skills 目录和脚本检查为准。
|
|
19
23
|
|
|
20
24
|
## 方法论
|
|
21
25
|
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
- `progress/README.md` — 进度交接日志
|
|
32
36
|
- `reports/README.md` — 验证与审计报告
|
|
33
37
|
- `decisions/README.md` — 架构决策
|
|
34
|
-
- `skills/README.md` — repo-local skill registry and vetting notes
|
|
38
|
+
- `skills/README.md` — repo-local skill registry and vetting notes
|
|
35
39
|
- `templates/` — 可复用的规划、报告与 DAG 模板
|
|
36
40
|
|
|
37
41
|
## 仓库 Skills
|
|
@@ -46,10 +50,10 @@
|
|
|
46
50
|
- `templates/sprint-contract.md` — 实现契约与验收标准
|
|
47
51
|
- `templates/exec-plan.md` — 非平凡工作的执行计划
|
|
48
52
|
- `templates/progress-log.md` — 进度与交接日志
|
|
49
|
-
- `templates/qa-report.md` — 验证与 QA 证据
|
|
50
|
-
- `templates/production-readiness-checklist.md` — 低/中风险单仓库 DAG readiness 检查清单
|
|
51
|
-
- `templates/init-evolution-review.md` — 初始化能力演化审查报告模板
|
|
52
|
-
- `templates/adr.md` — 架构决策记录(ADR)
|
|
53
|
+
- `templates/qa-report.md` — 验证与 QA 证据
|
|
54
|
+
- `templates/production-readiness-checklist.md` — 低/中风险单仓库 DAG readiness 检查清单
|
|
55
|
+
- `templates/init-evolution-review.md` — 初始化能力演化审查报告模板
|
|
56
|
+
- `templates/adr.md` — 架构决策记录(ADR)
|
|
53
57
|
|
|
54
58
|
## 维护
|
|
55
59
|
|
|
@@ -6,6 +6,8 @@ loop-agent 是面向 agentic coding 的工作流 runtime。仓库应保持小而
|
|
|
6
6
|
|
|
7
7
|
项目采用「人类掌舵、智能体执行」的工程模型。Agent 可实现、验证与总结,但持久意图与完成证据必须落在仓库中。
|
|
8
8
|
|
|
9
|
+
这个模型吸收了 Anthropic 长时运行 agent harness、OpenAI Codex harness engineering、腾讯端到端 Harness Engineering 和社区 agent harness 实践中的共同经验:用短入口文档导航长期知识,用结构化任务状态和 handoff artifact 跨 session 传递事实,用独立验证和评审回路约束 agent 输出,并把人类注意力集中在意图、边界、架构和反馈系统上。面向使用者的背景资料收录在 `website/docs/practices/`。
|
|
10
|
+
|
|
9
11
|
- 仓库是记录系统。决策、契约、计划、测试、报告与交接属于 tracked files。
|
|
10
12
|
- `AGENTS.md` 是操作地图,不是知识 dump。长期方法论与决策属于 `docs/`。
|
|
11
13
|
- 工作以小步、可逆、可验证的增量推进。
|
|
@@ -9,3 +9,11 @@ npm 包携带本 README 作为目录契约。具体 completed plan 属于目标
|
|
|
9
9
|
- [`2026-07-04-runtime-boundary-remediation.md`](2026-07-04-runtime-boundary-remediation.md) — 整合 CLI/skill/runtime 边界,抽出 DAG/Loop runtime seam,集中 harness store/guard 策略
|
|
10
10
|
- [`2026-07-04-dag-role-skill-alignment.md`](2026-07-04-dag-role-skill-alignment.md) — 对齐 DAG/Dynamic Workflow role 与 repo-local vetted skills,新增 strict skill audit
|
|
11
11
|
- [`2026-07-06-production-readiness-hardening.md`](2026-07-06-production-readiness-hardening.md) — 冻结 Production Readiness v0.1,打磨 DAG 主路径 next steps、failure routing、doctor/report/failure handoff 与 dogfood 验证
|
|
12
|
+
- [`2026-07-08-taskspec-schema-validate.md`](2026-07-08-taskspec-schema-validate.md) — 新增 TaskSpec v0.1 schema、三层校验器、risk→complexity 映射和 5 个 dogfood TaskSpec 样例
|
|
13
|
+
- [`2026-07-08-acceptance-taskgraph-ready.md`](2026-07-08-acceptance-taskgraph-ready.md) — 新增 AcceptanceSpec / TaskGraphSpec schema、Feature graph 校验、环检测和 Ready 队列计算
|
|
14
|
+
- [`2026-07-08-profile-mapping-agent-worker-cli.md`](2026-07-08-profile-mapping-agent-worker-cli.md) — 新增 business profile 到 loop-agent profile 映射、独立 `agent-worker` CLI 和 package bin surface
|
|
15
|
+
- [`2026-07-08-loop-agent-client-contract-smoke.md`](2026-07-08-loop-agent-client-contract-smoke.md) — 新增 LoopAgentClient、command artifact、JSON parse failure、repo resolver/preflight 与 Ring 0 CLI contract smoke
|
|
16
|
+
- [`2026-07-08-materializer-ring1-dag.md`](2026-07-08-materializer-ring1-dag.md) — 新增 TaskSpec materializer、materialize manifest、coverage-safe source 输出与 Ring 1 DAG generate/validate/dry-run smoke
|
|
17
|
+
- [`2026-07-08-run-task-pipeline-ring2.md`](2026-07-08-run-task-pipeline-ring2.md) — 新增 Worker run-task pipeline、task-local run record、成功/失败分流与 Ring 2 shell-only DAG success/failure smoke
|
|
18
|
+
- [`2026-07-08-task-pool-morning-report.md`](2026-07-08-task-pool-morning-report.md) — 新增 Worker Task Pool JSONL/state store、failure routing、串行 batch run-ready 和 morning report
|
|
19
|
+
- [`2026-07-08-taskspec-worker-master.md`](2026-07-08-taskspec-worker-master.md) — 完成 TaskSpec + Worker 7 步 master 编排,覆盖 schema、graph、profile、client、materialize、run-task、Task Pool 和 morning report
|