@tea-agent/loop-agent 0.22.0 → 0.23.1

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 (73) hide show
  1. package/AGENTS.md +42 -108
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +7 -4
  4. package/dist/application/context-usage/skill-resolution-stats.js +263 -0
  5. package/dist/application/dag/generate-task-dag.js +17 -3
  6. package/dist/cli/command-definitions.js +8 -7
  7. package/dist/cli/program.js +17 -15
  8. package/dist/commands/doctor.js +269 -18
  9. package/dist/commands/init.js +101 -86
  10. package/dist/commands/stats.js +40 -11
  11. package/dist/shared/operator/capabilities.js +11 -1
  12. package/dist/worker/console/app-data.js +4 -0
  13. package/dist/worker/console/chat/instruction-skills.js +217 -0
  14. package/dist/worker/console/chat/model-resolver.js +106 -0
  15. package/dist/worker/console/chat/pi-runtime.js +605 -0
  16. package/dist/worker/console/chat/resource-loader.js +66 -0
  17. package/dist/worker/console/chat/routes.js +357 -0
  18. package/dist/worker/console/chat/session-store.js +238 -0
  19. package/dist/worker/console/chat/tool-adapter.js +238 -0
  20. package/dist/worker/console/chat/tools.js +171 -0
  21. package/dist/worker/console/server.js +55 -0
  22. package/dist/worker/console/static/assets/index-DVl7Jxt5.js +25 -0
  23. package/dist/worker/console/static/assets/index-lVcIr9Ju.css +1 -0
  24. package/dist/worker/console/static/index.html +2 -2
  25. package/dist/worker/feature/profile-schema.js +1 -1
  26. package/dist/workflows/dag/init-hybrid.js +69 -21
  27. package/dist/workflows/dag/node-execution.js +38 -1
  28. package/dist/workflows/dag/output-protocol.js +89 -0
  29. package/dist/workflows/dag/prompt.js +35 -1
  30. package/dist/workflows/dag/recovery-recommendation.js +45 -0
  31. package/dist/workflows/dag/report.js +28 -1
  32. package/dist/workflows/dag/rerun-task.js +1 -1
  33. package/dist/workflows/dag/scheduler.js +9 -0
  34. package/dist/workflows/dag/types.js +12 -0
  35. package/dist/workflows/dag/validate.js +55 -0
  36. package/docs/README.md +73 -156
  37. package/docs/architecture/README.md +3 -2
  38. package/docs/architecture/dag-execution.md +2 -2
  39. package/docs/architecture/evolution.md +14 -12
  40. package/docs/architecture/system-overview.md +1 -1
  41. package/docs/architecture/worker-and-feature.md +3 -3
  42. package/docs/governance/README.md +15 -0
  43. package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
  44. package/docs/init-surface.manifest.json +22 -4
  45. package/docs/operations/README.md +12 -0
  46. package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
  47. package/docs/skills/vetted-skill-registry.md +23 -3
  48. package/docs/templates/README.md +55 -0
  49. package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
  50. package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
  51. package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
  52. package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
  53. package/docs/templates/hybrid-dag.json +1 -1
  54. package/docs/templates/progress-log.md +9 -2
  55. package/harness.json +5 -5
  56. package/package.json +5 -5
  57. package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
  58. package/skills/agent-worker/SKILL.md +1 -1
  59. package/skills/grill-with-docs/SKILL.md +44 -52
  60. package/skills/grill-with-docs/adr-format.md +37 -26
  61. package/skills/grill-with-docs/context-format.md +18 -26
  62. package/skills/loop-agent/SKILL.md +28 -112
  63. package/skills/loop-agent/references/command-reference.md +9 -3
  64. package/skills/loop-agent/references/harness-policy.md +3 -3
  65. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  66. package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
  67. package/skills/loop-agent/references/task-workflow.md +2 -0
  68. package/skills/systematic-debugging/SKILL.md +20 -4
  69. package/skills/test-driven-development/SKILL.md +10 -3
  70. package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
  71. package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
  72. /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
  73. /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
@@ -184,9 +184,9 @@ export const COMMAND_DEFINITIONS = [
184
184
  adapter: "required",
185
185
  tier: "operator",
186
186
  intent: "Check local loop-agent runtime health.",
187
- usage: "doctor",
188
- handler: async ({ adapter, repoRoot }) => {
189
- await runDoctor(adapter, repoRoot);
187
+ usage: "doctor [--json] [--context]",
188
+ handler: async ({ adapter, repoRoot, rest }) => {
189
+ await runDoctor(adapter, repoRoot, rest.filter((arg) => Boolean(arg)));
190
190
  },
191
191
  },
192
192
  {
@@ -345,10 +345,11 @@ export const COMMAND_DEFINITIONS = [
345
345
  name: "stats",
346
346
  adapter: "required",
347
347
  tier: "operator",
348
- intent: "Inspect historical step duration statistics.",
349
- usage: "stats",
350
- handler: async ({ repoRoot }) => {
351
- await runStats(repoRoot);
348
+ intent: "Inspect historical step duration statistics, or skill-resolution context usage.",
349
+ usage: "stats | stats context [--last <n>] [--json]",
350
+ subcommands: ["context"],
351
+ handler: async ({ repoRoot, subcommand, rest }) => {
352
+ await runStats(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
352
353
  },
353
354
  },
354
355
  {
@@ -92,25 +92,27 @@ function commandChain(command) {
92
92
  }
93
93
  return names;
94
94
  }
95
- async function resolveDispatchContext(command, defaultRepoRoot) {
96
- const options = collectGlobalOptions(command);
97
- const repoRoot = path.resolve(options.repoRoot ?? defaultRepoRoot);
95
+ async function dispatchRegisteredCommand(commandObject, defaultRepoRoot, rawArgs) {
96
+ const chain = commandChain(commandObject);
97
+ const command = chain[0] ?? "";
98
+ const [subcommand, ...rest] = chain.length <= 1 ? rawArgs : [chain[1], ...chain.slice(2), ...rawArgs];
99
+ const options = collectGlobalOptions(commandObject);
100
+ const quiet = (command === "doctor" &&
101
+ [...rest, ...rawArgs].some((token) => token === "--json")) ||
102
+ (command === "stats" &&
103
+ subcommand === "context" &&
104
+ [...rest, ...rawArgs].some((token) => token === "--json"));
105
+ const repoRoot = resolveRepoRootOnly(commandObject, defaultRepoRoot, {
106
+ quiet,
107
+ });
98
108
  const adapter = await resolveAdapter(repoRoot, options.adapter);
99
- if (options.adapter)
109
+ if (options.adapter && !quiet)
100
110
  console.log(`[loop-agent] adapter: ${adapter.name}`);
101
- if (options.repoRoot)
102
- console.log(`[loop-agent] repo root: ${repoRoot}`);
103
- return {
111
+ const ctx = {
104
112
  adapter,
105
113
  repoRoot,
106
114
  explicitWorktree: options.worktree,
107
115
  };
108
- }
109
- async function dispatchRegisteredCommand(commandObject, defaultRepoRoot, rawArgs) {
110
- const chain = commandChain(commandObject);
111
- const command = chain[0] ?? "";
112
- const [subcommand, ...rest] = chain.length <= 1 ? rawArgs : [chain[1], ...chain.slice(2), ...rawArgs];
113
- const ctx = await resolveDispatchContext(commandObject, defaultRepoRoot);
114
116
  await runCommanderAction(ctx, command, subcommand, rest);
115
117
  }
116
118
  async function runCommanderAction(ctx, command, subcommand, rest) {
@@ -119,7 +121,7 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
119
121
  await runInspect(ctx.adapter, ctx.repoRoot);
120
122
  return;
121
123
  case "doctor":
122
- await runDoctor(ctx.adapter, ctx.repoRoot);
124
+ await runDoctor(ctx.adapter, ctx.repoRoot, compactArgs([subcommand, ...rest]));
123
125
  return;
124
126
  case "docs":
125
127
  if (subcommand === "audit") {
@@ -184,7 +186,7 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
184
186
  await runCloseout(ctx.repoRoot, compactArgs([subcommand, ...rest]));
185
187
  return;
186
188
  case "stats":
187
- await runStats(ctx.repoRoot);
189
+ await runStats(ctx.repoRoot, compactArgs([subcommand, ...rest]));
188
190
  return;
189
191
  case "plan":
190
192
  if (subcommand === "list" || subcommand === undefined) {
@@ -1,32 +1,283 @@
1
- import { access } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { checkPiAvailability, resolvePiBackend } from '../executors/pi-executor.js';
4
- import { checkPiSdkAvailability } from '../executors/pi-sdk-executor.js';
5
- export async function runDoctor(adapter, repoRoot) {
1
+ import os from "node:os";
2
+ import { access } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { checkPiAvailability, resolvePiBackend } from "../executors/pi-executor.js";
5
+ import { checkPiSdkAvailability } from "../executors/pi-sdk-executor.js";
6
+ import { getContextPolicy, } from "../workflows/dag/context-policy.js";
7
+ import { DEFAULT_SKILLS_BY_ROLE, } from "../workflows/dag/skills.js";
8
+ import { resolveDagSkillInstructions, } from "../workflows/dag/skill-instructions.js";
9
+ const ENTRYPOINT_KEYS = [
10
+ "readme",
11
+ "agents",
12
+ "governanceIndex",
13
+ "principles",
14
+ "workflow",
15
+ "verificationMatrix",
16
+ ];
17
+ const CONTEXT_AUDIT_ROLES = ["planner", "supervisor", "closeout"];
18
+ /** Parse `--flag` / `--flag=value` boolean flags from a raw argv token list. */
19
+ export function parseDoctorArgs(args) {
20
+ let json = false;
21
+ let context = false;
22
+ for (const token of args) {
23
+ if (token === "--json" || token === "--json=true")
24
+ json = true;
25
+ else if (token === "--context" || token === "--context=true")
26
+ context = true;
27
+ else if (token.startsWith("--context="))
28
+ context = token.slice("--context=".length) === "true";
29
+ else if (token.startsWith("--json="))
30
+ json = token.slice("--json=".length) === "true";
31
+ }
32
+ return { json, context };
33
+ }
34
+ function summarize(checks) {
35
+ let errors = 0;
36
+ let warnings = 0;
37
+ let infos = 0;
38
+ for (const check of checks) {
39
+ if (check.status === "error")
40
+ errors += 1;
41
+ else if (check.status === "warning")
42
+ warnings += 1;
43
+ else
44
+ infos += 1;
45
+ }
46
+ return { errors, warnings, infos };
47
+ }
48
+ async function collectManifestChecks(adapter, repoRoot) {
49
+ const checks = [];
6
50
  const manifest = await adapter.loadHarnessManifest(repoRoot);
7
51
  const requiredPaths = [];
8
- for (const key of ['readme', 'agents', 'governanceIndex', 'principles', 'workflow', 'verificationMatrix']) {
52
+ const entrypointLabels = [];
53
+ for (const key of ENTRYPOINT_KEYS) {
9
54
  const value = manifest.entrypoints[key];
10
- if (value)
55
+ if (value) {
11
56
  requiredPaths.push(value);
57
+ entrypointLabels.push(`${key}=${value}`);
58
+ }
12
59
  }
13
60
  const checkRepoScript = manifest.scripts?.checkRepo;
14
- if (checkRepoScript)
61
+ if (checkRepoScript) {
15
62
  requiredPaths.push(checkRepoScript);
16
- requiredPaths.push('.harness/prompts/analyze.md');
17
- requiredPaths.push('.harness/prompts/plan.md');
63
+ }
64
+ requiredPaths.push(".harness/prompts/analyze.md");
65
+ requiredPaths.push(".harness/prompts/plan.md");
18
66
  for (const rel of requiredPaths) {
19
- await access(path.join(repoRoot, rel));
67
+ try {
68
+ await access(path.join(repoRoot, rel));
69
+ }
70
+ catch {
71
+ checks.push({
72
+ id: `manifest-path:${rel}`,
73
+ status: "error",
74
+ message: `required harness path missing: ${rel}`,
75
+ });
76
+ }
20
77
  }
21
- const pi = checkPiAvailability();
22
- if (!pi.ok) {
23
- throw new Error(`pi unavailable: ${pi.detail}`);
78
+ checks.push({
79
+ id: "manifest-entrypoints",
80
+ status: "info",
81
+ message: `harness manifest entrypoints resolved`,
82
+ detail: { entrypoints: entrypointLabels },
83
+ });
84
+ return checks;
85
+ }
86
+ function collectExecutorChecks(piOk, piDetail, backendMode, sdkOk, sdkDetail) {
87
+ const checks = [];
88
+ if (!piOk) {
89
+ checks.push({
90
+ id: "executor:pi-cli",
91
+ status: "error",
92
+ message: `pi unavailable: ${piDetail}`,
93
+ });
94
+ return checks;
95
+ }
96
+ checks.push({
97
+ id: "executor:pi-cli",
98
+ status: "info",
99
+ message: `pi backend=${backendMode} cli=ok sdk=${sdkOk ? "ok" : "unavailable"}`,
100
+ });
101
+ if (backendMode === "sdk-first" && !sdkOk) {
102
+ checks.push({
103
+ id: "executor:pi-sdk",
104
+ status: "warning",
105
+ message: `CODE_AGENT_PI_BACKEND=sdk-first but pi SDK unavailable (${sdkDetail}); steps will fall back to CLI`,
106
+ });
24
107
  }
108
+ return checks;
109
+ }
110
+ async function collectContextAudit(repoRoot) {
111
+ const policyId = "baseline-v1";
112
+ const policy = getContextPolicy(policyId);
113
+ const manifest = policy.toManifest();
114
+ const roles = [];
115
+ for (const role of CONTEXT_AUDIT_ROLES) {
116
+ const skillNames = DEFAULT_SKILLS_BY_ROLE[role];
117
+ const budget = policy.resolveSkillInstructionBudget({
118
+ role,
119
+ executor: "pi",
120
+ });
121
+ const resolved = await resolveDagSkillInstructions([...skillNames], {
122
+ cwd: repoRoot,
123
+ env: {},
124
+ homeDir: os.homedir(),
125
+ perSkillMaxChars: budget.perSkillMaxChars,
126
+ totalMaxChars: budget.totalMaxChars,
127
+ includeLearnedPatterns: budget.includeLearnedPatterns,
128
+ });
129
+ const skills = resolved.map((entry) => toContextAuditSkill(entry));
130
+ roles.push({ role, skills });
131
+ }
132
+ return { policy: manifest, roles };
133
+ }
134
+ function toContextAuditSkill(entry) {
135
+ return {
136
+ name: entry.name,
137
+ resolution: entry.resolution,
138
+ entryTruncated: Boolean(entry.truncated),
139
+ ...(entry.references
140
+ ? {
141
+ references: entry.references.map((ref) => ({
142
+ name: ref.name,
143
+ sha256: ref.sha256,
144
+ bytes: ref.bytes,
145
+ truncated: Boolean(ref.truncated),
146
+ })),
147
+ }
148
+ : {}),
149
+ ...(entry.unresolvedReferences
150
+ ? { unresolvedReferences: entry.unresolvedReferences }
151
+ : {}),
152
+ };
153
+ }
154
+ function contextAuditToChecks(audit) {
155
+ const checks = [];
156
+ for (const roleEntry of audit.roles) {
157
+ for (const skill of roleEntry.skills) {
158
+ if (skill.resolution === "missing" || skill.resolution === "error") {
159
+ checks.push({
160
+ id: `context:${roleEntry.role}:${skill.name}:resolution`,
161
+ status: "error",
162
+ message: `role ${roleEntry.role} skill ${skill.name} did not resolve (${skill.resolution})`,
163
+ });
164
+ }
165
+ if (skill.entryTruncated) {
166
+ checks.push({
167
+ id: `context:${roleEntry.role}:${skill.name}:entry-truncated`,
168
+ status: "warning",
169
+ message: `role ${roleEntry.role} skill ${skill.name} entry truncated under default budget`,
170
+ });
171
+ }
172
+ for (const ref of skill.references ?? []) {
173
+ if (ref.truncated) {
174
+ checks.push({
175
+ id: `context:${roleEntry.role}:${skill.name}:ref:${ref.name}:truncated`,
176
+ status: "warning",
177
+ message: `role ${roleEntry.role} skill ${skill.name} reference ${ref.name} excerpt truncated under default budget`,
178
+ });
179
+ }
180
+ }
181
+ for (const unresolved of skill.unresolvedReferences ?? []) {
182
+ if (unresolved.reason === "budget") {
183
+ checks.push({
184
+ id: `context:${roleEntry.role}:${skill.name}:ref:${unresolved.name ?? unresolved.path}:budget`,
185
+ status: "error",
186
+ message: `role ${roleEntry.role} skill ${skill.name} reference ${unresolved.name ?? unresolved.path} skipped: reason=budget`,
187
+ });
188
+ }
189
+ else if (unresolved.reason === "missing" ||
190
+ unresolved.reason === "read-error" ||
191
+ unresolved.reason === "cycle") {
192
+ checks.push({
193
+ id: `context:${roleEntry.role}:${skill.name}:ref:${unresolved.name ?? unresolved.path}:${unresolved.reason}`,
194
+ status: "error",
195
+ message: `role ${roleEntry.role} skill ${skill.name} reference ${unresolved.name ?? unresolved.path} unresolved: reason=${unresolved.reason}`,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return checks;
202
+ }
203
+ /**
204
+ * Build the typed DoctorReport without side effects. This is the single fact
205
+ * source shared by both the human-readable and JSON renderers in runDoctor.
206
+ */
207
+ export async function buildDoctorReport(adapter, repoRoot, options = {}) {
208
+ const checks = [];
209
+ checks.push(...(await collectManifestChecks(adapter, repoRoot)));
210
+ const pi = checkPiAvailability();
25
211
  const backendMode = resolvePiBackend();
26
212
  const sdk = await checkPiSdkAvailability(repoRoot);
27
- console.log(`doctor: pi backend=${backendMode} cli=${pi.ok ? 'ok' : 'fail'} sdk=${sdk.ok ? 'ok' : 'unavailable'}`);
28
- if (backendMode === 'sdk-first' && !sdk.ok) {
29
- console.error(`doctor warning: CODE_AGENT_PI_BACKEND=sdk-first but pi SDK unavailable (${sdk.detail}); steps will fall back to CLI`);
213
+ checks.push(...collectExecutorChecks(pi.ok, pi.detail, backendMode, sdk.ok, sdk.detail));
214
+ if (options.context) {
215
+ const audit = await collectContextAudit(repoRoot);
216
+ checks.push(...contextAuditToChecks(audit));
217
+ return {
218
+ schemaVersion: 1,
219
+ ok: !checks.some((check) => check.status === "error"),
220
+ command: "doctor",
221
+ checks,
222
+ summary: summarize(checks),
223
+ contextAudit: audit,
224
+ };
225
+ }
226
+ return {
227
+ schemaVersion: 1,
228
+ ok: !checks.some((check) => check.status === "error"),
229
+ command: "doctor",
230
+ checks,
231
+ summary: summarize(checks),
232
+ };
233
+ }
234
+ function renderHumanReport(report) {
235
+ for (const check of report.checks) {
236
+ const tag = check.status === "error"
237
+ ? "ERROR"
238
+ : check.status === "warning"
239
+ ? "warn"
240
+ : "info";
241
+ const line = check.detail !== undefined
242
+ ? `doctor [${tag}] ${check.id}: ${check.message}`
243
+ : `doctor [${tag}] ${check.id}: ${check.message}`;
244
+ if (check.status === "error") {
245
+ console.error(line);
246
+ }
247
+ else {
248
+ console.log(line);
249
+ }
250
+ }
251
+ if (report.contextAudit) {
252
+ console.log(`doctor context: policy=${report.contextAudit.policy.policyId} roles=${report.contextAudit.roles.map((role) => role.role).join(",")}`);
253
+ for (const role of report.contextAudit.roles) {
254
+ for (const skill of role.skills) {
255
+ const refs = (skill.references ?? [])
256
+ .map((ref) => `${ref.name}${ref.truncated ? "*" : ""}`)
257
+ .join(",");
258
+ console.log(` ${role.role}/${skill.name}: resolution=${skill.resolution} entryTruncated=${skill.entryTruncated}${refs ? ` refs=${refs}` : ""}`);
259
+ }
260
+ }
261
+ }
262
+ console.log(`doctor: ${report.ok ? "ok" : "errors"} (errors=${report.summary.errors} warnings=${report.summary.warnings} infos=${report.summary.infos})`);
263
+ }
264
+ /**
265
+ * Run the doctor command. Renders a human-readable summary by default, or a
266
+ * single JSON document when `--json` is present. Both renderers consume the
267
+ * same typed DoctorReport. Returns the process exit code (1 on any error
268
+ * finding, 0 otherwise) and sets process.exitCode accordingly.
269
+ */
270
+ export async function runDoctor(adapter, repoRoot, args = []) {
271
+ const { json, context } = parseDoctorArgs(args);
272
+ const report = await buildDoctorReport(adapter, repoRoot, { context });
273
+ const exitCode = report.ok ? 0 : 1;
274
+ process.exitCode = exitCode;
275
+ if (json) {
276
+ // Machine envelope: stdout must contain only a single JSON document.
277
+ process.stdout.write(`${JSON.stringify(report)}\n`);
278
+ }
279
+ else {
280
+ renderHumanReport(report);
30
281
  }
31
- console.log('doctor ok');
282
+ return exitCode;
32
283
  }