@tea-agent/loop-agent 0.10.0-alpha.0 → 0.11.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.
Files changed (48) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +39 -47
  3. package/README.md +33 -6
  4. package/dist/application/dag/args.js +2 -3
  5. package/dist/application/dag/generate-task-dag.js +5 -14
  6. package/dist/cli/command-definitions.js +44 -5
  7. package/dist/cli/program.js +37 -3
  8. package/dist/cli/update/notifier.js +117 -0
  9. package/dist/cli/update/npm-client.js +151 -0
  10. package/dist/cli/update/policy.js +58 -0
  11. package/dist/cli/update/state.js +68 -0
  12. package/dist/cli.js +33 -0
  13. package/dist/commands/init.js +432 -58
  14. package/dist/commands/plan.js +50 -0
  15. package/dist/governance/exec-plans.js +545 -0
  16. package/dist/governance/manifest-types.js +0 -5
  17. package/dist/task/config-types.js +0 -1
  18. package/dist/worker/observe/static/app.js +326 -45
  19. package/dist/worker/observe/static/styles.css +1 -0
  20. package/dist/workflows/dag/governance-profile.js +0 -10
  21. package/dist/workflows/dag/init-hybrid.js +5 -201
  22. package/dist/workflows/dag/sdd-embedded.js +128 -0
  23. package/dist/workflows/dag/skill-instructions.js +5 -4
  24. package/docs/README.md +1 -0
  25. package/docs/agent-dag-runner.md +2 -2
  26. package/docs/architecture/runtime-boundaries.md +3 -0
  27. package/docs/design/README.md +1 -0
  28. package/docs/development-principles.md +1 -1
  29. package/docs/exec-plans/active/README.md +2 -2
  30. package/docs/exec-plans/completed/README.md +6 -0
  31. package/docs/feature-workflow.md +27 -21
  32. package/docs/harness-methodology-debugging.md +1 -1
  33. package/docs/harness-methodology-tdd.md +3 -3
  34. package/docs/init-surface.manifest.json +23 -50
  35. package/docs/loop-agent-harness.md +8 -3
  36. package/docs/progress/README.md +6 -0
  37. package/docs/reports/README.md +10 -0
  38. package/docs/templates/project-start-checklist.md +2 -2
  39. package/harness.json +1 -3
  40. package/package.json +3 -3
  41. package/skills/frontend-implementation/SKILL.md +3 -0
  42. package/skills/loop-agent/references/command-reference.md +6 -0
  43. package/skills/loop-agent/references/docs-converge.md +5 -5
  44. package/skills/loop-agent/references/task-workflow.md +1 -1
  45. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +0 -131
  46. package/docs/templates/backend-test-dag.json +0 -213
  47. package/docs/templates/backend-test-dag.retrospect.prompt.md +0 -128
  48. package/docs/templates/backend-test-dag.review-cases.prompt.md +0 -85
@@ -0,0 +1,151 @@
1
+ import { spawn } from "node:child_process";
2
+ import { lstat, readFile, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import semver from "semver";
5
+ const PACKAGE_NAME = "@tea-agent/loop-agent";
6
+ export function createNpmUpdateClient(options) {
7
+ const npmCommand = options.npmCommand ?? "npm";
8
+ const loopAgentCommand = options.loopAgentCommand ?? "loop-agent";
9
+ const runner = options.runner ?? runCommand;
10
+ const timeoutMs = options.timeoutMs ?? 1500;
11
+ const env = options.env;
12
+ return {
13
+ async latestVersion() {
14
+ const result = await runner(npmCommand, ["view", PACKAGE_NAME, "dist-tags.latest", "--json"], { timeoutMs, env });
15
+ if (result.exitCode !== 0 || result.timedOut)
16
+ return undefined;
17
+ return parseNpmViewLatest(result.stdout);
18
+ },
19
+ async canAutoInstall() {
20
+ return proveGlobalNpmInstall({
21
+ currentVersion: options.currentVersion,
22
+ packageRoot: options.packageRoot,
23
+ npmCommand,
24
+ runner,
25
+ env,
26
+ timeoutMs,
27
+ });
28
+ },
29
+ async installExact(version) {
30
+ if (!isStrictRelease(version))
31
+ return { ok: false, reason: "invalid-version" };
32
+ const result = await runner(npmCommand, buildExactInstallArgs(version), { timeoutMs: Math.max(timeoutMs, 120000), env });
33
+ if (result.exitCode !== 0 || result.timedOut)
34
+ return { ok: false, reason: "install-failed" };
35
+ return { ok: true };
36
+ },
37
+ async verifyInstalled(version) {
38
+ const list = await runner(npmCommand, ["list", "-g", PACKAGE_NAME, "--depth=0", "--json"], { timeoutMs, env });
39
+ if (list.exitCode !== 0 || !verifyNpmListVersion(list.stdout, version)) {
40
+ return { ok: false, reason: "npm-list-version-mismatch" };
41
+ }
42
+ const pathVersion = await runner(loopAgentCommand, ["--version"], { timeoutMs, env });
43
+ if (pathVersion.exitCode !== 0 || !verifyPathVersion(pathVersion.stdout, version)) {
44
+ return { ok: false, reason: "path-version-mismatch" };
45
+ }
46
+ return { ok: true };
47
+ },
48
+ };
49
+ }
50
+ export function parseNpmViewLatest(stdout) {
51
+ const trimmed = stdout.trim();
52
+ let value = trimmed;
53
+ try {
54
+ value = JSON.parse(trimmed);
55
+ }
56
+ catch {
57
+ // npm may emit a bare value under older versions or mocked tests.
58
+ }
59
+ if (typeof value !== "string")
60
+ return undefined;
61
+ return isStrictRelease(value) ? value : undefined;
62
+ }
63
+ export function buildExactInstallArgs(version) {
64
+ if (!isStrictRelease(version))
65
+ throw new Error(`invalid release version: ${version}`);
66
+ return ["install", "-g", `${PACKAGE_NAME}@${version}`, "--no-fund", "--no-audit"];
67
+ }
68
+ export function verifyNpmListVersion(stdout, expectedVersion) {
69
+ try {
70
+ const parsed = JSON.parse(stdout);
71
+ return parsed.dependencies?.[PACKAGE_NAME]?.version === expectedVersion;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ export function verifyPathVersion(stdout, expectedVersion) {
78
+ return stdout.trim() === expectedVersion;
79
+ }
80
+ async function proveGlobalNpmInstall(options) {
81
+ const root = await options.runner(options.npmCommand, ["root", "-g"], {
82
+ timeoutMs: options.timeoutMs,
83
+ env: options.env,
84
+ });
85
+ if (root.exitCode !== 0 || root.timedOut)
86
+ return { ok: false, reason: "npm-root-failed" };
87
+ const globalPackageRoot = path.join(root.stdout.trim(), "@tea-agent", "loop-agent");
88
+ try {
89
+ const [expectedReal, currentReal, expectedStat] = await Promise.all([
90
+ realpath(globalPackageRoot),
91
+ realpath(options.packageRoot),
92
+ lstat(globalPackageRoot),
93
+ ]);
94
+ if (expectedStat.isSymbolicLink())
95
+ return { ok: false, reason: "linked-global-package" };
96
+ if (expectedReal !== currentReal)
97
+ return { ok: false, reason: "not-global" };
98
+ const pkg = JSON.parse(await readFile(path.join(options.packageRoot, "package.json"), "utf-8"));
99
+ if (pkg.name !== PACKAGE_NAME)
100
+ return { ok: false, reason: "wrong-package" };
101
+ if (pkg.version !== options.currentVersion)
102
+ return { ok: false, reason: "version-mismatch" };
103
+ return { ok: true };
104
+ }
105
+ catch {
106
+ return { ok: false, reason: "source-proof-failed" };
107
+ }
108
+ }
109
+ function isStrictRelease(version) {
110
+ const parsed = semver.parse(version);
111
+ return Boolean(parsed && parsed.prerelease.length === 0);
112
+ }
113
+ async function runCommand(command, args, options) {
114
+ return new Promise((resolve) => {
115
+ const child = spawn(command, [...args], {
116
+ env: options.env ? { ...process.env, ...options.env } : process.env,
117
+ stdio: ["ignore", "pipe", "pipe"],
118
+ shell: false,
119
+ });
120
+ let stdout = "";
121
+ let stderr = "";
122
+ let settled = false;
123
+ const timer = setTimeout(() => {
124
+ settled = true;
125
+ child.kill("SIGTERM");
126
+ resolve({ exitCode: null, stdout, stderr, timedOut: true });
127
+ }, options.timeoutMs);
128
+ child.stdout.setEncoding("utf-8");
129
+ child.stderr.setEncoding("utf-8");
130
+ child.stdout.on("data", (chunk) => {
131
+ stdout += chunk;
132
+ });
133
+ child.stderr.on("data", (chunk) => {
134
+ stderr += chunk;
135
+ });
136
+ child.on("error", (error) => {
137
+ if (settled)
138
+ return;
139
+ settled = true;
140
+ clearTimeout(timer);
141
+ resolve({ exitCode: null, stdout, stderr: `${stderr}${error.message}`, timedOut: false });
142
+ });
143
+ child.on("close", (exitCode) => {
144
+ if (settled)
145
+ return;
146
+ settled = true;
147
+ clearTimeout(timer);
148
+ resolve({ exitCode, stdout, stderr, timedOut: false });
149
+ });
150
+ });
151
+ }
@@ -0,0 +1,58 @@
1
+ import semver from "semver";
2
+ const CONTROLLER_SENSITIVE_COMMANDS = new Set([
3
+ "run-dag",
4
+ "dag",
5
+ "loop",
6
+ "delegate",
7
+ "pi-prompt",
8
+ "cursor-prompt",
9
+ ]);
10
+ const MACHINE_OUTPUT_FLAGS = new Set(["--json", "--markdown"]);
11
+ export function isStrictReleaseVersion(version) {
12
+ if (!version)
13
+ return false;
14
+ const parsed = semver.parse(version);
15
+ return Boolean(parsed && parsed.prerelease.length === 0);
16
+ }
17
+ export function shouldCheckForUpdates(input) {
18
+ if (!input.stdinIsTTY || !input.stdoutIsTTY || !input.stderrIsTTY)
19
+ return false;
20
+ if (input.env.CI || input.env.LOOP_AGENT_DISABLE_UPDATE_CHECK === "1")
21
+ return false;
22
+ const args = input.argv;
23
+ if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V")) {
24
+ return false;
25
+ }
26
+ if (args.some((arg) => MACHINE_OUTPUT_FLAGS.has(arg)))
27
+ return false;
28
+ const command = firstCommand(args);
29
+ if (command && CONTROLLER_SENSITIVE_COMMANDS.has(command))
30
+ return false;
31
+ return true;
32
+ }
33
+ export function evaluateLatestVersion(input) {
34
+ const now = input.now ?? new Date();
35
+ if (input.retryAfter && Date.parse(input.retryAfter) > now.getTime()) {
36
+ return { shouldPrompt: false, reason: "retry-after" };
37
+ }
38
+ if (!isStrictReleaseVersion(input.currentVersion)) {
39
+ return { shouldPrompt: false, reason: "invalid-current-version" };
40
+ }
41
+ if (!isStrictReleaseVersion(input.latestVersion)) {
42
+ return { shouldPrompt: false, reason: "invalid-latest-version" };
43
+ }
44
+ if (!semver.gt(input.latestVersion, input.currentVersion)) {
45
+ return { shouldPrompt: false, reason: "not-newer" };
46
+ }
47
+ if (input.dismissedVersion === input.latestVersion) {
48
+ return { shouldPrompt: false, reason: "dismissed-version" };
49
+ }
50
+ return { shouldPrompt: true, targetVersion: input.latestVersion };
51
+ }
52
+ function firstCommand(argv) {
53
+ for (const arg of argv) {
54
+ if (!arg.startsWith("-"))
55
+ return arg;
56
+ }
57
+ return undefined;
58
+ }
@@ -0,0 +1,68 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export function updateStatePath(homeDir) {
4
+ return path.join(homeDir, ".loop-agent", "update-state.json");
5
+ }
6
+ export function updateLockPath(homeDir) {
7
+ return path.join(homeDir, ".loop-agent", "update.lock");
8
+ }
9
+ export async function readUpdateState(statePath) {
10
+ try {
11
+ const raw = await readFile(statePath, "utf-8");
12
+ const parsed = JSON.parse(raw);
13
+ if (parsed.schemaVersion !== 1)
14
+ return emptyState();
15
+ return {
16
+ schemaVersion: 1,
17
+ lastCheckedAt: stringOrUndefined(parsed.lastCheckedAt),
18
+ latestVersion: stringOrUndefined(parsed.latestVersion),
19
+ dismissedVersion: stringOrUndefined(parsed.dismissedVersion),
20
+ retryAfter: stringOrUndefined(parsed.retryAfter),
21
+ };
22
+ }
23
+ catch {
24
+ return emptyState();
25
+ }
26
+ }
27
+ export async function writeUpdateState(statePath, state) {
28
+ await mkdir(path.dirname(statePath), { recursive: true });
29
+ const tempPath = path.join(path.dirname(statePath), `.${path.basename(statePath)}.${process.pid}.${Date.now()}.tmp`);
30
+ await writeFile(tempPath, `${JSON.stringify({ ...state, schemaVersion: 1 }, null, 2)}\n`, "utf-8");
31
+ await rename(tempPath, statePath);
32
+ }
33
+ export async function acquireUpdateLock(lockPath, options = {}) {
34
+ const now = options.now ?? (() => new Date());
35
+ const staleMs = options.staleMs ?? 10 * 60 * 1000;
36
+ await mkdir(path.dirname(lockPath), { recursive: true });
37
+ try {
38
+ await mkdir(lockPath);
39
+ await writeFile(path.join(lockPath, "owner.json"), JSON.stringify({ pid: process.pid, at: now().toISOString() }), "utf-8");
40
+ return async () => {
41
+ await rm(lockPath, { recursive: true, force: true });
42
+ };
43
+ }
44
+ catch {
45
+ try {
46
+ const ownerRaw = await readFile(path.join(lockPath, "owner.json"), "utf-8");
47
+ const owner = JSON.parse(ownerRaw);
48
+ if (owner.at && now().getTime() - Date.parse(owner.at) > staleMs) {
49
+ await rm(lockPath, { recursive: true, force: true });
50
+ await mkdir(lockPath);
51
+ await writeFile(path.join(lockPath, "owner.json"), JSON.stringify({ pid: process.pid, at: now().toISOString() }), "utf-8");
52
+ return async () => {
53
+ await rm(lockPath, { recursive: true, force: true });
54
+ };
55
+ }
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ return undefined;
61
+ }
62
+ }
63
+ function emptyState() {
64
+ return { schemaVersion: 1 };
65
+ }
66
+ function stringOrUndefined(value) {
67
+ return typeof value === "string" ? value : undefined;
68
+ }
package/dist/cli.js CHANGED
@@ -1,12 +1,45 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
2
6
  import { buildLoopAgentProgram } from "./cli/program.js";
7
+ import { createNpmUpdateClient } from "./cli/update/npm-client.js";
8
+ import { runSelfUpdateNotifier } from "./cli/update/notifier.js";
3
9
  async function main() {
4
10
  const program = buildLoopAgentProgram({
5
11
  defaultRepoRoot: process.cwd(),
6
12
  });
7
13
  await program.parseAsync(process.argv);
14
+ if (process.exitCode && process.exitCode !== 0)
15
+ return;
16
+ const packageRoot = resolvePackageRoot();
17
+ const currentVersion = readPackageVersion(packageRoot);
18
+ await runSelfUpdateNotifier({
19
+ argv: process.argv.slice(2),
20
+ currentVersion,
21
+ env: process.env,
22
+ homeDir: os.homedir(),
23
+ stdinIsTTY: process.stdin.isTTY === true,
24
+ stdoutIsTTY: process.stdout.isTTY === true,
25
+ stderrIsTTY: process.stderr.isTTY === true,
26
+ client: createNpmUpdateClient({
27
+ currentVersion,
28
+ packageRoot,
29
+ env: process.env,
30
+ }),
31
+ });
8
32
  }
9
33
  main().catch((error) => {
10
34
  console.error(error instanceof Error ? error.message : String(error));
11
35
  process.exit(1);
12
36
  });
37
+ function resolvePackageRoot() {
38
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
39
+ }
40
+ function readPackageVersion(packageRoot) {
41
+ const pkg = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf-8"));
42
+ if (!pkg.version)
43
+ throw new Error("loop-agent package.json is missing version");
44
+ return pkg.version;
45
+ }