@tea-agent/loop-agent 0.17.1 → 0.18.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/CHANGELOG.md +96 -22
- package/dist/application/dag/args.js +9 -2
- package/dist/executors/dag-pi-executor.js +11 -0
- package/dist/executors/shell-executor.js +62 -2
- package/dist/governance/spine-audit.js +5 -2
- package/dist/worker/console/draft-store.js +81 -3
- package/dist/worker/console/interview/grill-me.js +253 -0
- package/dist/worker/console/operation-runner.js +2 -1
- package/dist/worker/console/operator-actions.js +609 -20
- package/dist/worker/console/pi-readiness.js +2 -0
- package/dist/worker/console/prd-identity.js +102 -0
- package/dist/worker/console/resolve-dag-run-for-task.js +76 -0
- package/dist/worker/console/server.js +11 -2
- package/dist/worker/console/static/assets/index-KUSib7aM.js +16 -0
- package/dist/worker/console/static/assets/index-ucIzpaGJ.css +1 -0
- package/dist/worker/console/static/index.html +3 -3
- package/dist/worker/console/workflow-kinds.js +46 -0
- package/dist/worker/observe/routes.js +8 -1
- package/dist/worker/observe/static/index.html +8 -8
- package/dist/worker/observe/static/styles.css +296 -181
- package/dist/workflows/dag/backend-test-intake-context.js +148 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +528 -0
- package/dist/workflows/dag/init-hybrid.js +93 -109
- package/dist/workflows/dag/types.js +4 -0
- package/dist/workflows/loop/benchmark.js +1 -1
- package/docs/README.md +84 -39
- package/docs/architecture/README.md +8 -4
- package/docs/architecture/dag-execution.md +8 -1
- package/docs/architecture/evolution.md +38 -25
- package/docs/architecture/facts-and-state.md +2 -0
- package/docs/architecture/system-overview.md +6 -5
- package/docs/architecture/worker-and-feature.md +15 -8
- package/docs/init-surface.manifest.json +10 -1
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
- package/docs/templates/backend-test-dag.json +74 -219
- package/docs/templates/branch-merge-report.md +1 -1
- package/docs/templates/exec-plan.md +2 -0
- package/docs/templates/progress-log.md +3 -0
- package/package.json +1 -1
- package/skills/init-capability-evolution/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/index-BEIdBogJ.js +0 -9
- package/dist/worker/console/static/assets/index-Rt0TqimP.css +0 -1
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
const SETUP_BLOCKED = Object.freeze([
|
|
7
7
|
"newTask",
|
|
8
8
|
"importPrd",
|
|
9
|
+
"bootstrapFromPrd",
|
|
9
10
|
"contractApply",
|
|
10
11
|
"contractAdopt",
|
|
11
12
|
"dagRunTask",
|
|
@@ -13,6 +14,7 @@ const SETUP_BLOCKED = Object.freeze([
|
|
|
13
14
|
"prepareDagConfirmation",
|
|
14
15
|
"interviewStart",
|
|
15
16
|
"interviewTurn",
|
|
17
|
+
"interviewSkip",
|
|
16
18
|
]);
|
|
17
19
|
const DEGRADED_BLOCKED = Object.freeze([
|
|
18
20
|
"contractApply",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive Console task title + canonical taskId from imported PRD markdown.
|
|
3
|
+
* Deterministic (no LLM): first ATX heading, else first meaningful line.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { formatLocalBusinessDate } from "../../task/runtime.js";
|
|
7
|
+
const GENERIC_SLUGS = new Set([
|
|
8
|
+
"test",
|
|
9
|
+
"fix",
|
|
10
|
+
"tmp",
|
|
11
|
+
"new-task",
|
|
12
|
+
"requirement",
|
|
13
|
+
"req",
|
|
14
|
+
"prd",
|
|
15
|
+
"untitled",
|
|
16
|
+
]);
|
|
17
|
+
function cleanTitleLine(line) {
|
|
18
|
+
return line
|
|
19
|
+
.replace(/^#{1,6}\s+/, "")
|
|
20
|
+
.replace(/^>\s+/, "")
|
|
21
|
+
.replace(/^[*_\s]+|[*_\s]+$/g, "")
|
|
22
|
+
.replace(/\s+/g, " ")
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
function isNoiseLine(line) {
|
|
26
|
+
const t = line.trim();
|
|
27
|
+
if (!t)
|
|
28
|
+
return true;
|
|
29
|
+
if (/^---+$/.test(t))
|
|
30
|
+
return true;
|
|
31
|
+
if (/^```/.test(t))
|
|
32
|
+
return true;
|
|
33
|
+
if (/^<!--/.test(t))
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
export function extractPrdTitle(content) {
|
|
38
|
+
const lines = content.split(/\r?\n/);
|
|
39
|
+
for (const raw of lines) {
|
|
40
|
+
const trimmed = raw.trim();
|
|
41
|
+
if (!/^#\s+\S/.test(trimmed))
|
|
42
|
+
continue;
|
|
43
|
+
const title = cleanTitleLine(trimmed);
|
|
44
|
+
if (title)
|
|
45
|
+
return { title: title.slice(0, 120), source: "heading" };
|
|
46
|
+
}
|
|
47
|
+
for (const raw of lines) {
|
|
48
|
+
if (isNoiseLine(raw))
|
|
49
|
+
continue;
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
if (/^#{2,6}\s+/.test(trimmed))
|
|
52
|
+
continue;
|
|
53
|
+
const title = cleanTitleLine(trimmed);
|
|
54
|
+
if (title.length >= 2) {
|
|
55
|
+
return { title: title.slice(0, 120), source: "first-line" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const raw of lines) {
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
if (!/^#{2,6}\s+\S/.test(trimmed))
|
|
61
|
+
continue;
|
|
62
|
+
const title = cleanTitleLine(trimmed);
|
|
63
|
+
if (title)
|
|
64
|
+
return { title: title.slice(0, 120), source: "heading" };
|
|
65
|
+
}
|
|
66
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
67
|
+
return { title: `需求任务 ${hash}`, source: "content-hash" };
|
|
68
|
+
}
|
|
69
|
+
export function asciiSlugFromTitle(title, contentForFallback) {
|
|
70
|
+
const ascii = title
|
|
71
|
+
.normalize("NFKD")
|
|
72
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
75
|
+
.replace(/^-+|-+$/g, "")
|
|
76
|
+
.replace(/-{2,}/g, "-")
|
|
77
|
+
.slice(0, 48);
|
|
78
|
+
if (ascii.length >= 3 && !GENERIC_SLUGS.has(ascii))
|
|
79
|
+
return ascii;
|
|
80
|
+
const hash = createHash("sha256")
|
|
81
|
+
.update(contentForFallback || title)
|
|
82
|
+
.digest("hex")
|
|
83
|
+
.slice(0, 8);
|
|
84
|
+
return `req-${hash}`;
|
|
85
|
+
}
|
|
86
|
+
export function deriveTaskIdentityFromPrd(content, now = new Date()) {
|
|
87
|
+
const extracted = extractPrdTitle(content);
|
|
88
|
+
const slug = asciiSlugFromTitle(extracted.title, content);
|
|
89
|
+
const date = formatLocalBusinessDate(now);
|
|
90
|
+
return {
|
|
91
|
+
title: extracted.title,
|
|
92
|
+
slug,
|
|
93
|
+
taskId: `${date}-${slug}`,
|
|
94
|
+
source: extracted.source,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function nextTaskIdRevision(taskId, revision) {
|
|
98
|
+
if (revision <= 1)
|
|
99
|
+
return taskId;
|
|
100
|
+
const base = taskId.replace(/-r\d+$/, "");
|
|
101
|
+
return `${base}-r${revision}`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the newest DAG run id for a harness task under `.harness/dag-runs`.
|
|
3
|
+
* Used by Console observeLink so task-scoped opens land on `#/dag/<runId>`.
|
|
4
|
+
*/
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
const LIFECYCLE_ORDER = ["active", "paused", "completed"];
|
|
8
|
+
async function readTaskIdFromRunDir(runDir) {
|
|
9
|
+
const runJsonPath = path.join(runDir, "run.json");
|
|
10
|
+
try {
|
|
11
|
+
const raw = JSON.parse(await readFile(runJsonPath, "utf8"));
|
|
12
|
+
const fromBinding = raw.taskContractBinding?.taskId?.trim() ||
|
|
13
|
+
raw.sourceBinding?.taskId?.trim();
|
|
14
|
+
if (fromBinding)
|
|
15
|
+
return fromBinding;
|
|
16
|
+
const title = raw.title?.trim() ?? "";
|
|
17
|
+
const hybrid = /^Hybrid DAG:\s*(.+)$/i.exec(title);
|
|
18
|
+
if (hybrid?.[1]?.trim())
|
|
19
|
+
return hybrid[1].trim();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* missing/invalid run.json — skip */
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Prefer active/paused over completed; within a lifecycle, newest mtime wins.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveLatestDagRunIdForTask(repoRoot, taskId) {
|
|
30
|
+
const wanted = taskId.trim();
|
|
31
|
+
if (!wanted)
|
|
32
|
+
return undefined;
|
|
33
|
+
const matches = [];
|
|
34
|
+
for (const lifecycle of LIFECYCLE_ORDER) {
|
|
35
|
+
const lifecycleDir = path.join(repoRoot, ".harness", "dag-runs", lifecycle);
|
|
36
|
+
let entries = [];
|
|
37
|
+
try {
|
|
38
|
+
entries = await readdir(lifecycleDir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const name of entries) {
|
|
44
|
+
if (!name || name === ".gitkeep")
|
|
45
|
+
continue;
|
|
46
|
+
const runDir = path.join(lifecycleDir, name);
|
|
47
|
+
let st;
|
|
48
|
+
try {
|
|
49
|
+
st = await stat(runDir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!st.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
const boundTaskId = await readTaskIdFromRunDir(runDir);
|
|
57
|
+
if (boundTaskId !== wanted)
|
|
58
|
+
continue;
|
|
59
|
+
matches.push({
|
|
60
|
+
dagRunId: name,
|
|
61
|
+
lifecycle,
|
|
62
|
+
mtimeMs: st.mtimeMs,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (matches.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const lifecycleRank = (value) => LIFECYCLE_ORDER.indexOf(value);
|
|
69
|
+
matches.sort((a, b) => {
|
|
70
|
+
const life = lifecycleRank(a.lifecycle) - lifecycleRank(b.lifecycle);
|
|
71
|
+
if (life !== 0)
|
|
72
|
+
return life;
|
|
73
|
+
return b.mtimeMs - a.mtimeMs;
|
|
74
|
+
});
|
|
75
|
+
return matches[0];
|
|
76
|
+
}
|
|
@@ -48,6 +48,7 @@ export async function createConsoleServer(options) {
|
|
|
48
48
|
const readiness = await probePiReadiness({
|
|
49
49
|
override: options.piReadinessProbe,
|
|
50
50
|
});
|
|
51
|
+
let readinessCache = readiness;
|
|
51
52
|
const artifactRoot = path.join(appData.root, "client-artifacts", appData.fingerprint);
|
|
52
53
|
const client = options.createClient?.(artifactRoot) ??
|
|
53
54
|
new LoopAgentClient({
|
|
@@ -58,7 +59,14 @@ export async function createConsoleServer(options) {
|
|
|
58
59
|
if (!options.skipReconcile) {
|
|
59
60
|
await operations.reconcileOnBoot();
|
|
60
61
|
}
|
|
61
|
-
const getReadiness = () =>
|
|
62
|
+
const getReadiness = () => readinessCache;
|
|
63
|
+
const refreshReadiness = async () => {
|
|
64
|
+
readinessCache = await probePiReadiness({
|
|
65
|
+
override: options.piReadinessProbe,
|
|
66
|
+
});
|
|
67
|
+
actionContext.readiness = readinessCache;
|
|
68
|
+
return readinessCache;
|
|
69
|
+
};
|
|
62
70
|
const actionContext = {
|
|
63
71
|
repoRoot,
|
|
64
72
|
appData,
|
|
@@ -70,8 +78,9 @@ export async function createConsoleServer(options) {
|
|
|
70
78
|
confirmations,
|
|
71
79
|
interviews,
|
|
72
80
|
assessments,
|
|
73
|
-
readiness,
|
|
81
|
+
readiness: readinessCache,
|
|
74
82
|
getReadiness,
|
|
83
|
+
refreshReadiness,
|
|
75
84
|
observeBaseUrl: options.observeBaseUrl,
|
|
76
85
|
fetchImpl: options.fetchImpl,
|
|
77
86
|
};
|