@tea-agent/loop-agent 0.22.0 → 0.24.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 (103) hide show
  1. package/AGENTS.md +42 -108
  2. package/CHANGELOG.md +85 -0
  3. package/README.md +8 -5
  4. package/bin/agent-worker.js +0 -0
  5. package/dist/application/context-usage/skill-resolution-stats.js +263 -0
  6. package/dist/application/dag/generate-task-dag.js +17 -3
  7. package/dist/cli/command-definitions.js +8 -7
  8. package/dist/cli/program.js +17 -15
  9. package/dist/commands/doctor.js +269 -18
  10. package/dist/commands/init.js +101 -86
  11. package/dist/commands/stats.js +40 -11
  12. package/dist/executors/shell-executor.js +20 -7
  13. package/dist/shared/operator/capabilities.js +486 -3
  14. package/dist/worker/console/app-data.js +6 -0
  15. package/dist/worker/console/chat/artifact-card.js +23 -0
  16. package/dist/worker/console/chat/chat-event-store.js +495 -0
  17. package/dist/worker/console/chat/chat-ui-policy.js +25 -0
  18. package/dist/worker/console/chat/composer-draft-store.js +45 -0
  19. package/dist/worker/console/chat/context-panel.js +54 -0
  20. package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
  21. package/dist/worker/console/chat/explore-tools.js +299 -0
  22. package/dist/worker/console/chat/human-gate-card.js +37 -0
  23. package/dist/worker/console/chat/instruction-skills.js +217 -0
  24. package/dist/worker/console/chat/interview-adapter.js +136 -0
  25. package/dist/worker/console/chat/model-resolver.js +106 -0
  26. package/dist/worker/console/chat/operation-card.js +23 -0
  27. package/dist/worker/console/chat/pi-console-config.js +158 -0
  28. package/dist/worker/console/chat/pi-runtime.js +1143 -0
  29. package/dist/worker/console/chat/repo-browser.js +140 -0
  30. package/dist/worker/console/chat/repo-walk.js +116 -0
  31. package/dist/worker/console/chat/resource-loader.js +67 -0
  32. package/dist/worker/console/chat/routes.js +1646 -0
  33. package/dist/worker/console/chat/runtime-context.js +24 -0
  34. package/dist/worker/console/chat/runtime-selection.js +37 -0
  35. package/dist/worker/console/chat/session-store.js +437 -0
  36. package/dist/worker/console/chat/shortcuts.js +15 -0
  37. package/dist/worker/console/chat/tool-adapter.js +125 -0
  38. package/dist/worker/console/chat/tools.js +195 -0
  39. package/dist/worker/console/chat/usage.js +37 -0
  40. package/dist/worker/console/chat/workspace-landing.js +56 -0
  41. package/dist/worker/console/dag-confirmation.js +42 -8
  42. package/dist/worker/console/human-gate-token.js +130 -0
  43. package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
  44. package/dist/worker/console/operation-runner.js +6 -2
  45. package/dist/worker/console/operation-sse.js +26 -0
  46. package/dist/worker/console/operator-actions.js +420 -7
  47. package/dist/worker/console/server.js +68 -1
  48. package/dist/worker/console/static/assets/index-BTbrEHnO.css +1 -0
  49. package/dist/worker/console/static/assets/index-D9qLevoP.js +27 -0
  50. package/dist/worker/console/static/index.html +2 -2
  51. package/dist/worker/feature/profile-schema.js +1 -1
  52. package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -5
  53. package/dist/workflows/dag/backend-test-result-contract.js +229 -0
  54. package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
  55. package/dist/workflows/dag/init-hybrid.js +71 -22
  56. package/dist/workflows/dag/node-execution.js +38 -1
  57. package/dist/workflows/dag/output-protocol.js +89 -0
  58. package/dist/workflows/dag/prompt.js +35 -1
  59. package/dist/workflows/dag/recovery-recommendation.js +45 -0
  60. package/dist/workflows/dag/report.js +28 -1
  61. package/dist/workflows/dag/rerun-task.js +1 -1
  62. package/dist/workflows/dag/scheduler.js +9 -0
  63. package/dist/workflows/dag/types.js +12 -0
  64. package/dist/workflows/dag/validate.js +55 -0
  65. package/docs/README.md +73 -156
  66. package/docs/architecture/README.md +7 -6
  67. package/docs/architecture/dag-execution.md +2 -2
  68. package/docs/architecture/evolution.md +16 -14
  69. package/docs/architecture/system-overview.md +1 -1
  70. package/docs/architecture/worker-and-feature.md +3 -3
  71. package/docs/governance/README.md +15 -0
  72. package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
  73. package/docs/init-surface.manifest.json +22 -4
  74. package/docs/operations/README.md +12 -0
  75. package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
  76. package/docs/skills/vetted-skill-registry.md +23 -3
  77. package/docs/templates/README.md +55 -0
  78. package/docs/templates/backend-test-dag.json +2 -2
  79. package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
  80. package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
  81. package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
  82. package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
  83. package/docs/templates/hybrid-dag.json +1 -1
  84. package/docs/templates/progress-log.md +9 -2
  85. package/harness.json +4 -4
  86. package/package.json +5 -5
  87. package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
  88. package/skills/agent-worker/SKILL.md +1 -1
  89. package/skills/grill-with-docs/SKILL.md +44 -52
  90. package/skills/grill-with-docs/adr-format.md +37 -26
  91. package/skills/grill-with-docs/context-format.md +18 -26
  92. package/skills/loop-agent/SKILL.md +28 -112
  93. package/skills/loop-agent/references/command-reference.md +9 -3
  94. package/skills/loop-agent/references/harness-policy.md +3 -3
  95. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  96. package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
  97. package/skills/loop-agent/references/task-workflow.md +2 -0
  98. package/skills/systematic-debugging/SKILL.md +20 -4
  99. package/skills/test-driven-development/SKILL.md +10 -3
  100. package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
  101. package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
  102. /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
  103. /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Lightweight Human Gate receipts for mutation actions that are not DAG-bound
3
+ * (standaloneTaskRerun / workerTaskRetry). Mirrors contract-apply receipt
4
+ * semantics with an atomic prepared → dispatching → dispatched state machine
5
+ * so concurrent confirm POSTs cannot create duplicate operations.
6
+ */
7
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
8
+ import path from "node:path";
9
+ export class MutationGateReceiptStore {
10
+ dir;
11
+ constructor(appData) {
12
+ this.dir = path.join(appData.root, "mutation-gate-receipts");
13
+ }
14
+ file(id) {
15
+ return path.join(this.dir, `${id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
16
+ }
17
+ async prepare(input) {
18
+ const receipt = {
19
+ schemaVersion: 1,
20
+ state: "prepared",
21
+ ...input,
22
+ };
23
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
24
+ await writeFile(this.file(receipt.receiptId), `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
25
+ return receipt;
26
+ }
27
+ async get(id) {
28
+ try {
29
+ return JSON.parse(await readFile(this.file(id), "utf8"));
30
+ }
31
+ catch (error) {
32
+ if (error.code === "ENOENT")
33
+ return undefined;
34
+ throw error;
35
+ }
36
+ }
37
+ /**
38
+ * Atomically claim a prepared receipt for dispatch.
39
+ * Concurrent callers: only one gets reserved=true; others see
40
+ * already-dispatching / already-dispatched with the same receipt.
41
+ */
42
+ async tryReserve(id) {
43
+ const receipt = await this.get(id);
44
+ if (!receipt) {
45
+ return {
46
+ ok: false,
47
+ code: "NOT_FOUND",
48
+ message: `mutation gate receipt not found: ${id}`,
49
+ };
50
+ }
51
+ if (Date.parse(receipt.expiresAt) <= Date.now()) {
52
+ return {
53
+ ok: false,
54
+ code: "EXPIRED",
55
+ message: "mutation gate receipt expired",
56
+ receipt,
57
+ };
58
+ }
59
+ if (receipt.state === "dispatching") {
60
+ return {
61
+ ok: true,
62
+ receipt,
63
+ reserved: false,
64
+ reason: "already-dispatching",
65
+ };
66
+ }
67
+ if (receipt.state === "dispatched") {
68
+ return {
69
+ ok: true,
70
+ receipt,
71
+ reserved: false,
72
+ reason: "already-dispatched",
73
+ };
74
+ }
75
+ if (receipt.state !== "prepared") {
76
+ return {
77
+ ok: false,
78
+ code: "INVALID_STATE",
79
+ message: `mutation gate receipt is ${receipt.state}`,
80
+ receipt,
81
+ };
82
+ }
83
+ const next = {
84
+ ...receipt,
85
+ state: "dispatching",
86
+ reservedAt: new Date().toISOString(),
87
+ };
88
+ const claimed = await this.compareAndSwap(id, "prepared", next);
89
+ if (!claimed.ok) {
90
+ const current = claimed.current;
91
+ if (current?.state === "dispatching") {
92
+ return {
93
+ ok: true,
94
+ receipt: current,
95
+ reserved: false,
96
+ reason: "already-dispatching",
97
+ };
98
+ }
99
+ if (current?.state === "dispatched") {
100
+ return {
101
+ ok: true,
102
+ receipt: current,
103
+ reserved: false,
104
+ reason: "already-dispatched",
105
+ };
106
+ }
107
+ return {
108
+ ok: false,
109
+ code: "INVALID_STATE",
110
+ message: `mutation gate receipt raced into ${current?.state ?? "missing"}`,
111
+ receipt: current,
112
+ };
113
+ }
114
+ return { ok: true, receipt: next, reserved: true };
115
+ }
116
+ async markDispatched(id, operationId) {
117
+ const receipt = await this.get(id);
118
+ if (!receipt)
119
+ throw new Error(`receipt not found: ${id}`);
120
+ const next = {
121
+ ...receipt,
122
+ state: "dispatched",
123
+ operationId,
124
+ dispatchedAt: new Date().toISOString(),
125
+ };
126
+ await writeFile(this.file(id), `${JSON.stringify(next, null, 2)}\n`, {
127
+ mode: 0o600,
128
+ });
129
+ return next;
130
+ }
131
+ /** Best-effort unlock if dispatch failed after reserve. */
132
+ async releaseToPrepared(id) {
133
+ const receipt = await this.get(id);
134
+ if (!receipt || receipt.state !== "dispatching")
135
+ return receipt;
136
+ const next = {
137
+ ...receipt,
138
+ state: "prepared",
139
+ reservedAt: undefined,
140
+ };
141
+ await writeFile(this.file(id), `${JSON.stringify(next, null, 2)}\n`, {
142
+ mode: 0o600,
143
+ });
144
+ return next;
145
+ }
146
+ async compareAndSwap(id, expectedState, next) {
147
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
148
+ const target = this.file(id);
149
+ const lockPath = `${target}.lock`;
150
+ try {
151
+ await writeFile(lockPath, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
152
+ }
153
+ catch (error) {
154
+ if (error.code === "EEXIST") {
155
+ // Brief spin for the other writer; then re-read.
156
+ await new Promise((r) => setTimeout(r, 25));
157
+ return { ok: false, current: await this.get(id) };
158
+ }
159
+ throw error;
160
+ }
161
+ try {
162
+ const current = await this.get(id);
163
+ if (!current || current.state !== expectedState) {
164
+ return { ok: false, current };
165
+ }
166
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
167
+ await writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, {
168
+ mode: 0o600,
169
+ });
170
+ await rename(tmp, target);
171
+ return { ok: true };
172
+ }
173
+ finally {
174
+ try {
175
+ await writeFile(lockPath, "", { flag: "w" });
176
+ const { unlink } = await import("node:fs/promises");
177
+ await unlink(lockPath);
178
+ }
179
+ catch {
180
+ // lock cleanup is best-effort
181
+ }
182
+ }
183
+ }
184
+ }
@@ -155,15 +155,19 @@ function coerceEnvelope(result) {
155
155
  /** Fire-and-forget scheduler for accepted operations. */
156
156
  export function scheduleOperation(operationId, deps) {
157
157
  void runOperation(operationId, deps).catch((error) => {
158
+ const message = error instanceof Error ? error.message : String(error);
158
159
  void deps.store
159
160
  .update(operationId, {
160
161
  state: "needs-reconcile",
161
162
  finishedAt: new Date().toISOString(),
162
163
  errorCode: "OPERATION_NEEDS_RECONCILE",
163
- errorMessage: error instanceof Error ? error.message : String(error),
164
+ errorMessage: message,
165
+ })
166
+ .then((updated) => {
167
+ stateEvent(deps.events, updated, "needs-reconcile", message);
164
168
  })
165
169
  .catch(() => {
166
- // ignore secondary failures
170
+ // A failed canonical store update cannot be represented safely.
167
171
  });
168
172
  });
169
173
  }
@@ -13,6 +13,7 @@ export function createOperationEventStore(options) {
13
13
  if (persistenceDir)
14
14
  ensureSecureDir(persistenceDir);
15
15
  const rings = new Map();
16
+ const subscribers = new Map();
16
17
  function persistedPath(operationId) {
17
18
  return persistenceDir
18
19
  ? path.join(persistenceDir, `${safeOperationId(operationId)}.json`)
@@ -91,6 +92,17 @@ export function createOperationEventStore(options) {
91
92
  ring.minSeq = ring.events[0].seq;
92
93
  }
93
94
  persist(operationId, ring);
95
+ const listeners = subscribers.get(operationId);
96
+ if (listeners) {
97
+ for (const listener of listeners) {
98
+ try {
99
+ listener(event);
100
+ }
101
+ catch {
102
+ // Subscriber failures must never break the canonical append path.
103
+ }
104
+ }
105
+ }
94
106
  return event;
95
107
  },
96
108
  listFrom(operationId, afterSeq) {
@@ -112,8 +124,22 @@ export function createOperationEventStore(options) {
112
124
  rings.set(operationId, ring);
113
125
  return ring?.events.slice() ?? [];
114
126
  },
127
+ subscribe(operationId, listener) {
128
+ let listeners = subscribers.get(operationId);
129
+ if (!listeners) {
130
+ listeners = new Set();
131
+ subscribers.set(operationId, listeners);
132
+ }
133
+ listeners.add(listener);
134
+ return () => {
135
+ listeners?.delete(listener);
136
+ if (listeners?.size === 0)
137
+ subscribers.delete(operationId);
138
+ };
139
+ },
115
140
  clear(operationId) {
116
141
  rings.delete(operationId);
142
+ subscribers.delete(operationId);
117
143
  const file = persistedPath(operationId);
118
144
  if (file)
119
145
  rmSync(file, { force: true });