@abelxiaoxing/cadence 1.8.3 → 2.0.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 (208) hide show
  1. package/README.md +324 -111
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -26
  4. package/agents/diagnosis-worker.md +14 -25
  5. package/agents/implementation-worker.md +16 -35
  6. package/config/.env.example +2 -5
  7. package/config/workflow-task.example.json +7 -0
  8. package/docs/remote-access.md +60 -0
  9. package/docs/reuse-and-distribution.md +44 -0
  10. package/docs/service-management.md +108 -0
  11. package/licenses/bundled-web.txt +6245 -0
  12. package/licenses/pi-web-ui-MIT.txt +21 -0
  13. package/licenses/pi-web-ui-reuse.md +22 -0
  14. package/package.json +94 -21
  15. package/src/index.ts +22 -1807
  16. package/src/native-build.ts +6 -0
  17. package/src/native-client.ts +103 -0
  18. package/src/native-export.ts +58 -0
  19. package/src/native-history.ts +359 -0
  20. package/src/operator-cli.mjs +453 -1937
  21. package/src/plan-proposal.ts +87 -0
  22. package/src/sdk-worker.mjs +549 -0
  23. package/src/service/contracts/active.ts +82 -0
  24. package/src/service/contracts/budget.ts +148 -0
  25. package/src/service/contracts/build.ts +67 -0
  26. package/src/service/contracts/capture.ts +48 -0
  27. package/src/service/contracts/core.ts +33 -0
  28. package/src/service/contracts/current.ts +261 -0
  29. package/src/service/contracts/domain.ts +68 -0
  30. package/src/service/contracts/history.ts +2245 -0
  31. package/src/service/contracts/models.ts +139 -0
  32. package/src/service/coordination.ts +862 -0
  33. package/src/service/private-files.ts +204 -0
  34. package/src/service-cli.mjs +5592 -0
  35. package/src/service-client.ts +747 -0
  36. package/src/service-contract.ts +245 -0
  37. package/src/service-host.mjs +22382 -0
  38. package/src/service-http-client.ts +425 -0
  39. package/src/service-wire.ts +372 -0
  40. package/src/task-shell.ts +1045 -0
  41. package/src/task-store.ts +479 -0
  42. package/src/task-types.ts +184 -0
  43. package/src/verification-diagnostics.ts +29 -72
  44. package/src/worker-capture.mjs +1197 -0
  45. package/src/worker-public.mjs +1753 -0
  46. package/src/worker-redaction.mjs +608 -0
  47. package/src/worker-tools.mjs +202 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  50. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  65. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  71. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  77. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  80. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  92. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  95. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  98. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  101. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  108. package/web/dist/assets/cadence.css +2 -0
  109. package/web/dist/assets/cadence.js +7 -0
  110. package/web/dist/assets/markdown.js +296 -0
  111. package/web/dist/assets/react.js +9 -0
  112. package/web/dist/assets/rolldown-runtime.js +1 -0
  113. package/web/dist/assets/timeline-client.js +41 -0
  114. package/web/dist/assets/vis-timeline.css +1274 -0
  115. package/web/dist/assets/vis-timeline.js +32 -0
  116. package/web/dist/index.html +18 -0
  117. package/web/dist/manifest.json +505 -0
  118. package/config/plan-draft.example.json +0 -125
  119. package/config/plan-draft.multiple-tasks.example.json +0 -375
  120. package/config/plan-draft.quick.example.json +0 -78
  121. package/config/routes.example.json +0 -39
  122. package/prompts/abel-design.md +0 -382
  123. package/prompts/abel-diagnose.md +0 -71
  124. package/prompts/abel-implement.md +0 -284
  125. package/prompts/abel-init.md +0 -52
  126. package/src/activation.ts +0 -44
  127. package/src/activity-contracts.ts +0 -82
  128. package/src/agent-registry.ts +0 -53
  129. package/src/apply-transaction.ts +0 -1437
  130. package/src/artifact-store.ts +0 -213
  131. package/src/build-windows-job.mjs +0 -95
  132. package/src/candidate-artifact.ts +0 -143
  133. package/src/candidate-patch.ts +0 -271
  134. package/src/candidate-workspace.ts +0 -80
  135. package/src/canonical.ts +0 -16
  136. package/src/change-contract-schema.ts +0 -191
  137. package/src/change-contract.ts +0 -324
  138. package/src/change-verification.ts +0 -1617
  139. package/src/child-budget.ts +0 -58
  140. package/src/child-model.ts +0 -142
  141. package/src/child-session.ts +0 -640
  142. package/src/contracts.ts +0 -2476
  143. package/src/control-contracts.ts +0 -382
  144. package/src/delivery-compiler.ts +0 -1805
  145. package/src/delivery-revision.ts +0 -42
  146. package/src/design-control.ts +0 -1299
  147. package/src/design-diagnostics.ts +0 -181
  148. package/src/design-journal.ts +0 -1773
  149. package/src/directory-sync.ts +0 -36
  150. package/src/durable-contracts.ts +0 -231
  151. package/src/durable-workflow.ts +0 -978
  152. package/src/evidence-draft.ts +0 -122
  153. package/src/execution-profile.ts +0 -64
  154. package/src/execution-retention.ts +0 -77
  155. package/src/file-snapshot.ts +0 -163
  156. package/src/implement-continuation.ts +0 -559
  157. package/src/implement-graph.ts +0 -690
  158. package/src/implement-plan.ts +0 -72
  159. package/src/install-config.d.mts +0 -6
  160. package/src/isolation-backend.ts +0 -353
  161. package/src/model-source.ts +0 -22
  162. package/src/openspec-cli.ts +0 -494
  163. package/src/operator-cli-entry.mjs +0 -24
  164. package/src/operator-tools.ts +0 -309
  165. package/src/package-candidate.ts +0 -227
  166. package/src/package-delivery.ts +0 -486
  167. package/src/package-state.ts +0 -320
  168. package/src/package-verification.ts +0 -635
  169. package/src/package-workflow.ts +0 -342
  170. package/src/packet-runtime.ts +0 -570
  171. package/src/parent-provider.ts +0 -375
  172. package/src/phase-execution.ts +0 -2259
  173. package/src/pi-adapter.ts +0 -19
  174. package/src/plan-draft-summary.ts +0 -527
  175. package/src/plan-draft.ts +0 -502
  176. package/src/remove-path.ts +0 -32
  177. package/src/route-policy.ts +0 -545
  178. package/src/run-state.ts +0 -420
  179. package/src/run-store.ts +0 -1179
  180. package/src/safe-path.ts +0 -77
  181. package/src/scoped-grep-worker.mjs +0 -23
  182. package/src/scoped-tools.ts +0 -775
  183. package/src/single-task-draft.ts +0 -142
  184. package/src/sqlite-schema.ts +0 -171
  185. package/src/state-root.ts +0 -215
  186. package/src/storage-schema.ts +0 -360
  187. package/src/subagent-activity.ts +0 -1417
  188. package/src/submit-tool.ts +0 -950
  189. package/src/task-ledger.ts +0 -1338
  190. package/src/transport-budget.ts +0 -52
  191. package/src/verification-capability.ts +0 -1191
  192. package/src/verification-environment-io.ts +0 -389
  193. package/src/verification-environment.ts +0 -162
  194. package/src/verification-identity.ts +0 -77
  195. package/src/verification-scan.ts +0 -64
  196. package/src/windows-job-backend.ts +0 -419
  197. package/src/windows-job.c +0 -149
  198. package/src/worker-broker.ts +0 -806
  199. package/src/workflow-engine.ts +0 -28
  200. package/src/workflow-policy.ts +0 -1679
  201. package/src/workflow-recovery-policy.ts +0 -97
  202. package/src/workflow-scheduling.ts +0 -105
  203. package/src/workflow-state-machine.ts +0 -4355
  204. package/src/workflow-status.ts +0 -576
  205. package/src/workspace-io-entry.mjs +0 -76
  206. package/src/workspace-io-worker.mjs +0 -1312
  207. package/src/workspace-io.ts +0 -89
  208. package/src/workspace-store.ts +0 -634
@@ -0,0 +1,1045 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ Key,
8
+ matchesKey,
9
+ truncateToWidth,
10
+ wrapTextWithAnsi,
11
+ } from "@earendil-works/pi-tui";
12
+ import { MANAGEMENT_GUIDANCE, NativeConnection } from "./native-client.ts";
13
+ import {
14
+ NativeHistory,
15
+ type NativeViews,
16
+ pretty,
17
+ value,
18
+ } from "./native-history.ts";
19
+ import { proposePlan } from "./plan-proposal.ts";
20
+ import { ProjectionTracker } from "./service-client.ts";
21
+ import {
22
+ type Action,
23
+ type Assignment,
24
+ type ContextVersion,
25
+ ContractError,
26
+ type CurrentView,
27
+ canonicalJson,
28
+ decodeAction,
29
+ LIMITS,
30
+ parseWireJson,
31
+ type Receipt,
32
+ roleForPurpose,
33
+ type Scope,
34
+ } from "./service-contract.ts";
35
+ import { diagnosticText } from "./verification-diagnostics.ts";
36
+
37
+ /** One transient raw Enter occurrence, not an approval or model-callable control. */
38
+ export class TerminalEntryLatch {
39
+ private entry: { text: string; at: number } | undefined;
40
+ private pasting = false;
41
+ clear(): void {
42
+ this.entry = undefined;
43
+ }
44
+ input(
45
+ data: string,
46
+ editor: string,
47
+ eligible: boolean,
48
+ now = Date.now(),
49
+ ): void {
50
+ this.clear();
51
+ if (data.includes("\x1b[200~")) this.pasting = true;
52
+ if (data.includes("\x1b[201~")) {
53
+ this.pasting = false;
54
+ return;
55
+ }
56
+ if (this.pasting || !eligible || !matchesKey(data, Key.enter)) return;
57
+ if (/^\/cadence(?:[ \t]+[^\r\n]*)?$/.test(editor))
58
+ this.entry = { text: editor, at: now };
59
+ }
60
+ consume(args: string, now = Date.now()): boolean {
61
+ const entry = this.entry;
62
+ this.clear();
63
+ return (
64
+ !!entry &&
65
+ now - entry.at >= 0 &&
66
+ now - entry.at < 1500 &&
67
+ entry.text.replace(/^\/cadence[ \t]*/, "") === args
68
+ );
69
+ }
70
+ }
71
+ function eligible(ctx: ExtensionContext): boolean {
72
+ return (
73
+ ctx.mode === "tui" &&
74
+ ctx.hasUI &&
75
+ ctx.isIdle() &&
76
+ !ctx.hasPendingMessages() &&
77
+ ctx.isProjectTrusted()
78
+ );
79
+ }
80
+
81
+ export function registerTaskShell(pi: ExtensionAPI): void {
82
+ const latch = new TerminalEntryLatch(),
83
+ connection = new NativeConnection();
84
+ let epoch = 0,
85
+ modal = false;
86
+ let unsubscribe: (() => void) | undefined, dismiss: (() => void) | undefined;
87
+ let abort = new AbortController();
88
+ let selectedTask: string | undefined;
89
+ const commandIds: string[] = [];
90
+ const depart = () => {
91
+ epoch++;
92
+ latch.clear();
93
+ abort.abort();
94
+ dismiss?.();
95
+ // Detach original attachment, not service jobs/provider/auth. Shutdown does
96
+ // not wait for their lifetime. Retained transport blocks uncertain reuse.
97
+ void connection.detach().catch(() => {});
98
+ };
99
+ pi.on("session_start", (_event, ctx) => {
100
+ depart();
101
+ unsubscribe?.();
102
+ unsubscribe = undefined;
103
+ selectedTask = undefined;
104
+ if (ctx.mode === "tui")
105
+ unsubscribe = ctx.ui.onTerminalInput((data) => {
106
+ latch.input(data, ctx.ui.getEditorText(), !modal && eligible(ctx));
107
+ return undefined;
108
+ });
109
+ });
110
+ pi.on("session_shutdown", () => {
111
+ depart();
112
+ unsubscribe?.();
113
+ unsubscribe = undefined;
114
+ });
115
+ pi.on("session_before_switch", depart);
116
+ pi.on("session_before_fork", depart);
117
+ pi.on("session_before_tree", depart);
118
+ pi.on("session_before_compact", depart);
119
+ pi.on("model_select", depart);
120
+ // Only local dialog input is owned. Detached service work never owns Pi's
121
+ // conversation/tool lifetime or silently cancels it.
122
+ pi.on("input", () => (modal ? { action: "handled" as const } : undefined));
123
+ pi.on("tool_call", () =>
124
+ modal
125
+ ? {
126
+ block: true,
127
+ reason:
128
+ "Cadence native dialog is active; Return detaches accepted work",
129
+ }
130
+ : undefined,
131
+ );
132
+ pi.on("user_bash", () =>
133
+ modal
134
+ ? {
135
+ result: {
136
+ output: "Cadence native dialog is active",
137
+ exitCode: 1,
138
+ cancelled: true,
139
+ truncated: false,
140
+ },
141
+ }
142
+ : undefined,
143
+ );
144
+
145
+ pi.registerCommand("cadence", {
146
+ description:
147
+ "Native service client: plan, Start, checks, review and retained history; Return detaches",
148
+ handler: async (args, ctx) => {
149
+ if (
150
+ !latch.consume(args) ||
151
+ modal ||
152
+ !eligible(ctx) ||
153
+ process.env.CADENCE_SUBAGENT_CHILD === "1"
154
+ )
155
+ return;
156
+ modal = true;
157
+ const generation = epoch,
158
+ sessionId = ctx.sessionManager.getSessionId(),
159
+ cwd = ctx.cwd;
160
+ const hostModel = JSON.stringify(ctx.model),
161
+ thinking = ctx.thinkingLevel;
162
+ abort = new AbortController();
163
+ const signal = abort.signal;
164
+ const current = () => {
165
+ if (
166
+ epoch !== generation ||
167
+ signal.aborted ||
168
+ !eligible(ctx) ||
169
+ ctx.cwd !== cwd ||
170
+ ctx.sessionManager.getSessionId() !== sessionId ||
171
+ JSON.stringify(ctx.model) !== hostModel ||
172
+ ctx.thinkingLevel !== thinking
173
+ )
174
+ throw new ContractError("stale");
175
+ };
176
+ const show = async (text: string) => {
177
+ current();
178
+ // Server data already has bounded pages. Sanitize, but never clip a
179
+ // retained page/graph into a purported full view (old 256KiB cap removed).
180
+ const clean = diagnosticText(text, Number.MAX_SAFE_INTEGER);
181
+ await ctx.ui.custom<void>((tui, _theme, _keys, done) => {
182
+ let offset = 0,
183
+ rows: string[] = [];
184
+ const close = () => done();
185
+ dismiss = close;
186
+ return {
187
+ render(width) {
188
+ rows = clean
189
+ .split("\n")
190
+ .flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width)));
191
+ const height = Math.max(1, Math.min(24, tui.terminal.rows - 4));
192
+ offset = Math.max(
193
+ 0,
194
+ Math.min(offset, Math.max(0, rows.length - height)),
195
+ );
196
+ return [
197
+ truncateToWidth(
198
+ `Cadence review • ${offset + 1}/${rows.length} • ↑↓ PgUp/PgDn Home/End • Esc return`,
199
+ width,
200
+ ),
201
+ ...rows.slice(offset, offset + height),
202
+ ];
203
+ },
204
+ invalidate() {},
205
+ handleInput(data) {
206
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter))
207
+ done();
208
+ else if (matchesKey(data, Key.up)) offset--;
209
+ else if (matchesKey(data, Key.down)) offset++;
210
+ else if (matchesKey(data, Key.pageUp)) offset -= 12;
211
+ else if (matchesKey(data, Key.pageDown)) offset += 12;
212
+ else if (matchesKey(data, Key.home)) offset = 0;
213
+ else if (matchesKey(data, Key.end)) offset = rows.length;
214
+ tui.requestRender();
215
+ },
216
+ dispose() {
217
+ if (dismiss === close) dismiss = undefined;
218
+ },
219
+ };
220
+ });
221
+ current();
222
+ };
223
+ const ui: NativeViews = {
224
+ signal,
225
+ current,
226
+ show,
227
+ async select(title, choices) {
228
+ let page = 0;
229
+ for (;;) {
230
+ current();
231
+ const paged = choices.length > 16;
232
+ const available = paged
233
+ ? choices.slice(page * 10, page * 10 + 10)
234
+ : choices;
235
+ let labels = available.map((c) =>
236
+ diagnosticText(c, Number.MAX_SAFE_INTEGER),
237
+ );
238
+ // Sanitization is presentation, not identity. Distinct choices can
239
+ // redact to identical text; never select the first by accident.
240
+ if (
241
+ new Set(labels).size !== labels.length ||
242
+ labels.some((label) =>
243
+ ["Next options page", "Previous options page"].includes(label),
244
+ )
245
+ )
246
+ labels = labels.map(
247
+ (label, i) => `${page * 10 + i + 1}. ${label}`,
248
+ );
249
+ const next = paged && (page + 1) * 10 < choices.length;
250
+ const r = await ctx.ui.select(
251
+ diagnosticText(
252
+ `${title}${paged ? ` • options page ${page + 1}` : ""}`,
253
+ Number.MAX_SAFE_INTEGER,
254
+ ),
255
+ [
256
+ ...labels,
257
+ ...(next ? ["Next options page"] : []),
258
+ ...(page ? ["Previous options page"] : []),
259
+ ],
260
+ { signal },
261
+ );
262
+ current();
263
+ if (r === "Next options page") {
264
+ page++;
265
+ continue;
266
+ }
267
+ if (r === "Previous options page") {
268
+ page--;
269
+ continue;
270
+ }
271
+ return r === undefined ? undefined : available[labels.indexOf(r)];
272
+ }
273
+ },
274
+ async input(title, prefill) {
275
+ current();
276
+ const r = await ctx.ui.input(title, prefill, { signal });
277
+ current();
278
+ return r;
279
+ },
280
+ async confirm(title, text) {
281
+ current();
282
+ const r = await ctx.ui.confirm(
283
+ title,
284
+ diagnosticText(text, Number.MAX_SAFE_INTEGER),
285
+ { signal },
286
+ );
287
+ current();
288
+ return r;
289
+ },
290
+ notify(text) {
291
+ ctx.ui.notify(diagnosticText(text, Number.MAX_SAFE_INTEGER), "info");
292
+ },
293
+ };
294
+ let attached = false;
295
+ try {
296
+ const client = await connection.attach(cwd);
297
+ current();
298
+ attached = true;
299
+ const session = connection.session;
300
+ if (!session) throw new ContractError("unavailable");
301
+ if (!connection.compatible()) ui.notify(MANAGEMENT_GUIDANCE);
302
+ const roster = value(await client.activeRuns(signal));
303
+ current();
304
+ if (
305
+ roster.storeId !== session.storeId ||
306
+ roster.epoch !== session.epoch
307
+ )
308
+ throw new ContractError("stale");
309
+ const workspaceReply = await client.workspace(signal);
310
+ current();
311
+ const workspace =
312
+ workspaceReply.kind === "ok" ? workspaceReply.value : roster;
313
+ if (
314
+ workspace.storeId !== session.storeId ||
315
+ workspace.epoch !== session.epoch ||
316
+ workspace.workspaceId !== roster.workspaceId
317
+ )
318
+ throw new ContractError("stale");
319
+ if (workspaceReply.kind === "error")
320
+ ui.notify(
321
+ "Workspace observation unavailable; independent active-run Cancel remains available. No action authority inferred.",
322
+ );
323
+ const history = new NativeHistory(client, session.storeId, ui);
324
+ const readCurrent = async (
325
+ taskId: string | null,
326
+ ): Promise<CurrentView> => {
327
+ const view = value(
328
+ await client.current({ storeId: session.storeId, taskId }, signal),
329
+ );
330
+ current();
331
+ if (
332
+ view.epoch !== session.epoch ||
333
+ view.workspaceId !== workspace.workspaceId
334
+ )
335
+ throw new ContractError("stale");
336
+ return view;
337
+ };
338
+ const authority = (view: CurrentView): ContextVersion => {
339
+ if (!connection.compatible() || view.context.status !== "available")
340
+ throw new ContractError("unavailable");
341
+ return view.context.version;
342
+ };
343
+ const fresh = async (expected: ContextVersion) => {
344
+ const observed = authority(
345
+ await readCurrent(expected.task?.id ?? null),
346
+ );
347
+ if (canonicalJson(observed) !== canonicalJson(expected))
348
+ throw new ContractError("stale");
349
+ };
350
+ const observeReceipt = async (id: string) => {
351
+ const result = await client.receipt(id, signal);
352
+ current();
353
+ await show(
354
+ `Command ${id} • receipt observation only; never replay\n${pretty(result)}`,
355
+ );
356
+ return result.status === "found" ? result.receipt : undefined;
357
+ };
358
+ const remember = (id: string) => {
359
+ commandIds.push(id);
360
+ if (commandIds.length > 64) commandIds.shift();
361
+ };
362
+ let observedRun: { taskId: string; runId: string } | undefined;
363
+ const publication = async (
364
+ initial: Receipt,
365
+ stopping = false,
366
+ ): Promise<Receipt> => {
367
+ if (!stopping && (initial.runId || initial.publication !== "pending"))
368
+ return initial;
369
+ return ctx.ui.custom<Receipt>((tui, _theme, _keys, done) => {
370
+ const readAbort = new AbortController();
371
+ let receipt = initial,
372
+ disposed = false,
373
+ busy = false,
374
+ observations = 0;
375
+ const close = () => done(receipt);
376
+ dismiss = close;
377
+ const poll = async () => {
378
+ if (disposed || busy) return;
379
+ busy = true;
380
+ try {
381
+ current();
382
+ const found = await client.receipt(
383
+ initial.commandId,
384
+ AbortSignal.any([signal, readAbort.signal]),
385
+ );
386
+ if (disposed) return;
387
+ current();
388
+ observations++;
389
+ if (found.status === "found") receipt = found.receipt;
390
+ tui.requestRender();
391
+ if (
392
+ (stopping
393
+ ? receipt.execution !== null
394
+ : receipt.publication !== "pending") ||
395
+ observations >= (stopping ? 3 : 10) ||
396
+ found.status !== "found"
397
+ )
398
+ close();
399
+ } catch {
400
+ if (!disposed) close();
401
+ } finally {
402
+ busy = false;
403
+ }
404
+ };
405
+ const timer = setInterval(() => void poll(), 1000);
406
+ const bound = setTimeout(
407
+ close,
408
+ stopping ? LIMITS.unconfirmedStopMs : 10000,
409
+ );
410
+ void poll();
411
+ return {
412
+ render(width) {
413
+ return [
414
+ truncateToWidth(
415
+ `Cadence command ${initial.commandId}`,
416
+ width,
417
+ ),
418
+ truncateToWidth(
419
+ `${stopping ? `Cancel settlement ${receipt.execution ?? "unconfirmed/pending"}` : `Publication ${receipt.publication}`} • Esc/Enter returns; no inferred stop`,
420
+ width,
421
+ ),
422
+ ];
423
+ },
424
+ invalidate() {},
425
+ handleInput(data) {
426
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape))
427
+ close();
428
+ },
429
+ dispose() {
430
+ disposed = true;
431
+ clearInterval(timer);
432
+ clearTimeout(bound);
433
+ readAbort.abort();
434
+ if (dismiss === close) dismiss = undefined;
435
+ },
436
+ };
437
+ });
438
+ };
439
+ const submit = async (
440
+ view: CurrentView,
441
+ action: Action,
442
+ ): Promise<Receipt | undefined> => {
443
+ const expected = authority(view);
444
+ await connection.refresh();
445
+ current();
446
+ if (!connection.session?.capabilities.actions)
447
+ throw new ContractError("unavailable");
448
+ await fresh(expected);
449
+ let changesText = "No task selected yet.";
450
+ if (expected.task) {
451
+ const changes = value(
452
+ await client.currentChanges(
453
+ { storeId: session.storeId, taskId: expected.task.id },
454
+ signal,
455
+ ),
456
+ );
457
+ current();
458
+ if (
459
+ changes.context.status !== "available" ||
460
+ canonicalJson(changes.context.version) !== canonicalJson(expected)
461
+ )
462
+ throw new ContractError("stale");
463
+ changesText = `Coverage ${pretty(changes.coverage)}\n${changes.text}`;
464
+ }
465
+ const prepared = value(await client.preflight(expected, action));
466
+ current();
467
+ const preview = prepared.preview;
468
+ await show(
469
+ `Exact action preview (no effects yet)\nReadable plan\n${preview.readablePlan}\n\nCurrent changes, including pre-existing dirt (not attribution)\n${changesText}\n\nComplete action/graph/identity preview\n${pretty(preview)}\n\nDirect tools, not a sandbox. Check PASS, native execution, coverage and human review are independent.\nReturn/quit detaches accepted work; only explicit Cancel requests stop.`,
470
+ );
471
+ const checkText =
472
+ action.kind === "check"
473
+ ? `\nExact shell command:\n${action.check.command}\nTimeout ${action.check.timeoutMs}ms\nEnvironment ${preview.checkEnvironmentPolicy} • generation ${expected.checkEnvGeneration}`
474
+ : "";
475
+ if (
476
+ !(await ui.confirm(
477
+ `Confirm ${action.kind} now?`,
478
+ `${expected.model.provider}/${expected.model.model} • ${expected.model.api} • thinking ${expected.model.thinking}\ncwd: ${JSON.stringify(preview.cwdLabel)}\nTask ${expected.task?.id ?? "new"} revision ${expected.task?.revision ?? "new"}\nIssues: ${preview.issues.join(", ") || "none in bounded preview"}${checkText}\nExpires ${prepared.expiresAt}\nConfirm the complete preview just displayed. Fresh native consent; no automatic retry or Git/install. Generate is advisory only, never Save/Start.`,
479
+ ))
480
+ )
481
+ return;
482
+ await fresh(expected);
483
+ current();
484
+ const commandId = randomUUID();
485
+ remember(commandId);
486
+ const base = { commandId, expected, grant: prepared.grant };
487
+ // No generic sender, retries or changed IDs after an uncertain reply.
488
+ const reply = await (() => {
489
+ switch (action.kind) {
490
+ case "create":
491
+ return client.create({ ...base, action });
492
+ case "save-plan":
493
+ return client.savePlan({ ...base, action });
494
+ case "generate-plan":
495
+ return client.generatePlan({ ...base, action });
496
+ case "start":
497
+ return client.start({ ...base, action });
498
+ case "check":
499
+ return client.check({ ...base, action });
500
+ case "review":
501
+ return client.review({ ...base, action });
502
+ }
503
+ })();
504
+ current();
505
+ if (reply.kind === "error") {
506
+ ui.notify(
507
+ `Command ${commandId}: ${pretty(reply.error)}. Observe this exact receipt; no automatic replay.`,
508
+ );
509
+ return observeReceipt(commandId);
510
+ }
511
+ ui.notify(
512
+ `Command ${commandId}\n${pretty(reply.value)}\nAccepted work survives Return and Pi quit.`,
513
+ );
514
+ const receipt = await publication(reply.value);
515
+ current();
516
+ if (receipt.runId && expected.task)
517
+ observedRun = { taskId: expected.task.id, runId: receipt.runId };
518
+ return receipt;
519
+ };
520
+ const cancel = async () => {
521
+ if (!connection.compatible(true))
522
+ throw new ContractError("unsupported-context");
523
+ const roster = value(await client.activeRuns(signal));
524
+ current();
525
+ if (roster.status !== "complete")
526
+ throw new ContractError("unavailable");
527
+ if (
528
+ roster.epoch !== session.epoch ||
529
+ roster.storeId !== session.storeId
530
+ )
531
+ throw new ContractError("stale");
532
+ const labels = roster.runs.map(
533
+ (r) =>
534
+ `Cancel ${r.runId} • task ${r.taskId} • ${r.kind} / ${r.stage}`,
535
+ );
536
+ const selected = await ui.select(
537
+ "Explicit Cancel • bounded stop, not Return; all active tasks (independent of history page)",
538
+ [...labels, "Return"],
539
+ );
540
+ if (!selected || !labels.includes(selected)) return;
541
+ const run = roster.runs[labels.indexOf(selected)],
542
+ commandId = randomUUID();
543
+ remember(commandId);
544
+ current();
545
+ const reply = await client.cancel({
546
+ commandId,
547
+ epoch: run.epoch,
548
+ runId: run.runId,
549
+ });
550
+ current();
551
+ const observation =
552
+ reply.kind === "ok" ? await publication(reply.value, true) : reply;
553
+ current();
554
+ await show(
555
+ `Explicit Cancel receipt ${commandId}\n${pretty(observation)}\nUnconfirmed/pending/unknown is not stopped; direct-child closure does not prove escaped descendants stopped. No automatic retry.`,
556
+ );
557
+ if (reply.kind === "error") await observeReceipt(commandId);
558
+ };
559
+ const chooseModel = async () => {
560
+ if (!connection.compatible())
561
+ throw new ContractError("unsupported-context");
562
+ const catalog = value(await client.models(signal));
563
+ current();
564
+ const labels = catalog.entries.map(
565
+ (e) =>
566
+ `${e.provider}/${e.model} • ${e.api ?? "unsupported"} • ${e.eligibility}`,
567
+ );
568
+ const selected = await ui.select(
569
+ `Exact OWNER model (Pi conversation model is independent) • ${catalog.status}\nCurrent ${pretty(catalog.selected)}`,
570
+ [...labels, "Return"],
571
+ );
572
+ if (!selected || !labels.includes(selected)) return;
573
+ const model = catalog.entries[labels.indexOf(selected)];
574
+ if (!model.api || model.eligibility !== "candidate")
575
+ throw new ContractError("unsupported-context");
576
+ const level = await ui.select(
577
+ "Exact thinking level • no clamping/fallback",
578
+ model.thinking.filter((t) => t.selectable).map((t) => t.level),
579
+ );
580
+ if (!level || level === "max") return;
581
+ // Decode via the shared model-choice method; never inspect parent auth.
582
+ const choice = {
583
+ provider: model.provider,
584
+ model: model.model,
585
+ api: model.api,
586
+ thinking: level as ContextVersion["model"]["thinking"],
587
+ };
588
+ if (!(await ui.confirm("Select exact owner model?", pretty(choice))))
589
+ return;
590
+ const reply = await client.chooseModel({
591
+ expected: catalog.version,
592
+ choice,
593
+ });
594
+ current();
595
+ await show(
596
+ `Model choice observation (no task run)\n${pretty(reply)}`,
597
+ );
598
+ await connection.refresh();
599
+ current();
600
+ };
601
+ const observe = async (scope: Scope) => {
602
+ await ctx.ui.custom<void>((tui, _theme, _keys, done) => {
603
+ const tracker = new ProjectionTracker(),
604
+ reading = new AbortController();
605
+ let text = "Reading owner…",
606
+ disposed = false,
607
+ busy = false;
608
+ const close = () => done();
609
+ dismiss = close;
610
+ const poll = async () => {
611
+ if (busy || disposed) return;
612
+ busy = true;
613
+ try {
614
+ current();
615
+ const ticket = tracker.beginSnapshot();
616
+ const readSignal = AbortSignal.any([signal, reading.signal]);
617
+ const active = await client.activeRuns(readSignal);
618
+ const reply = await client.snapshot(scope, readSignal);
619
+ if (disposed) return;
620
+ current();
621
+ if (reply.kind === "ok")
622
+ tracker.install(reply.value, Date.now(), ticket);
623
+ else
624
+ text = `Observation ${reply.error.code}; no outcome inferred`;
625
+ tracker.tick(Date.now());
626
+ if (reply.kind === "ok") {
627
+ const state = tracker.state;
628
+ text = `Connection ${state?.connection ?? "unavailable"} • committed ${reply.value.committedSeq}\n${reply.value.runs.map((r) => `${r.childRunId ? `Child ${r.assignmentId}` : `Parent ${r.admission.kind}`}: ${r.lifecycle} / ${r.outcome ?? "unsettled"} / ${r.publication}\nOwner ${active.kind === "ok" && active.value.status === "complete" ? (active.value.runs.some((a) => a.runId === r.runId) ? "active/draining" : "drained") : "activity unavailable"} • ID ${r.childRunId ?? r.runId}\ncoverage ${pretty(r.coverage)}`).join("\n")}\nFull fields, native facts and all retained pages: History / search / export. This compact live view is not the archive.`;
629
+ }
630
+ tui.requestRender();
631
+ } catch {
632
+ text = "Observation unavailable; accepted work may continue";
633
+ } finally {
634
+ busy = false;
635
+ }
636
+ };
637
+ const timer = setInterval(() => void poll(), LIMITS.heartbeatMs);
638
+ void poll();
639
+ return {
640
+ render(width) {
641
+ return [
642
+ truncateToWidth(
643
+ "Cadence owner observation • Esc/Enter return (work continues)",
644
+ width,
645
+ ),
646
+ ...diagnosticText(text, Number.MAX_SAFE_INTEGER)
647
+ .split("\n")
648
+ .flatMap((line) =>
649
+ wrapTextWithAnsi(line, Math.max(1, width)),
650
+ )
651
+ .slice(0, Math.max(1, Math.min(24, tui.terminal.rows - 4))),
652
+ ];
653
+ },
654
+ invalidate() {},
655
+ handleInput(data) {
656
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape))
657
+ done();
658
+ },
659
+ dispose() {
660
+ disposed = true;
661
+ clearInterval(timer);
662
+ reading.abort();
663
+ if (dismiss === close) dismiss = undefined;
664
+ },
665
+ };
666
+ });
667
+ current();
668
+ };
669
+ let taskId = selectedTask;
670
+ for (;;) {
671
+ const menu = await ui.select(
672
+ "Cadence service • old sessions must be stopped • Return/quit detaches; accepted work continues",
673
+ [
674
+ "New",
675
+ "Open / history",
676
+ ...(taskId ? ["Current task"] : []),
677
+ "Model / thinking",
678
+ "Active runs / Cancel",
679
+ "Command receipts",
680
+ "Service status / guidance",
681
+ "Return",
682
+ ],
683
+ );
684
+ if (!menu || menu === "Return") break;
685
+ if (menu === "Model / thinking") {
686
+ await chooseModel();
687
+ continue;
688
+ }
689
+ if (menu === "Active runs / Cancel") {
690
+ await cancel();
691
+ continue;
692
+ }
693
+ if (menu === "Command receipts") {
694
+ const id = await ui.select("Observe exact command ID (no replay)", [
695
+ ...commandIds,
696
+ "Enter command ID",
697
+ "Return",
698
+ ]);
699
+ if (id === "Enter command ID") {
700
+ const entered = await ui.input("Exact command UUID");
701
+ if (entered) await observeReceipt(entered);
702
+ } else if (id && id !== "Return") await observeReceipt(id);
703
+ continue;
704
+ }
705
+ if (menu === "Service status / guidance") {
706
+ await show(
707
+ `${MANAGEMENT_GUIDANCE}\n${pretty(await connection.refresh())}\n${pretty(value(await client.activeRuns(signal)))}\nRecovery is explicit native-local management; this menu never clears locks.`,
708
+ );
709
+ continue;
710
+ }
711
+ if (menu === "New") {
712
+ const view = await readCurrent(null);
713
+ if (view.context.status !== "available") {
714
+ ui.notify(
715
+ "Choose an exact owner Model / thinking first. No task was created.",
716
+ );
717
+ continue;
718
+ }
719
+ const kind = await ui.select("Task kind", [
720
+ "code",
721
+ "research",
722
+ "diagnosis",
723
+ ]);
724
+ if (kind !== "code" && kind !== "research" && kind !== "diagnosis")
725
+ continue;
726
+ const goal = await ctx.ui.editor(
727
+ "Goal (saving does not start work)",
728
+ args,
729
+ );
730
+ current();
731
+ if (!goal?.trim()) continue;
732
+ const receipt = await submit(view, {
733
+ kind: "create",
734
+ taskKind: kind,
735
+ goal,
736
+ });
737
+ if (
738
+ receipt?.admission !== "accepted" ||
739
+ !receipt.created ||
740
+ receipt.created.state !== "published"
741
+ )
742
+ continue;
743
+ taskId = receipt.created.taskId;
744
+ } else if (menu === "Open / history")
745
+ taskId = await history.pages(
746
+ "tasks",
747
+ { kind: "workspace", id: workspace.workspaceId },
748
+ null,
749
+ true,
750
+ );
751
+ if (!taskId) continue;
752
+ selectedTask = taskId;
753
+ for (;;) {
754
+ let view: CurrentView;
755
+ try {
756
+ view = await readCurrent(taskId);
757
+ } catch {
758
+ current();
759
+ const fallback = await ui.select(
760
+ "Current task unavailable; history is not mutation authority",
761
+ ["Active runs / Cancel", "History / search / export", "Return"],
762
+ );
763
+ if (fallback === "Active runs / Cancel") await cancel();
764
+ else if (fallback === "History / search / export")
765
+ await history.scope({ kind: "task", id: taskId });
766
+ else break;
767
+ continue;
768
+ }
769
+ const kind = view.task?.kind;
770
+ const action = await ui.select(
771
+ `${kind ?? "task"} ${taskId} • current v1 revision ${view.task?.revision ?? "unavailable"} • ${view.context.status}`,
772
+ [
773
+ "Plan / assignments",
774
+ "Generate plan (readonly)",
775
+ "Start / Continue",
776
+ "Observe work",
777
+ "Active runs / Cancel",
778
+ ...(kind === "code" ? ["Checks"] : []),
779
+ "Changes / obligations",
780
+ "Review / Done",
781
+ "History / search / export",
782
+ "Return",
783
+ ],
784
+ );
785
+ if (!action || action === "Return") break;
786
+ if (action === "Active runs / Cancel") {
787
+ await cancel();
788
+ continue;
789
+ }
790
+ if (action === "Observe work") {
791
+ await observe(
792
+ observedRun?.taskId === taskId
793
+ ? { kind: "run", id: observedRun.runId }
794
+ : { kind: "task", id: taskId },
795
+ );
796
+ continue;
797
+ }
798
+ if (action === "History / search / export") {
799
+ await history.scope({ kind: "task", id: taskId });
800
+ continue;
801
+ }
802
+ if (action === "Changes / obligations") {
803
+ const changes = value(
804
+ await client.currentChanges(
805
+ { storeId: session.storeId, taskId },
806
+ signal,
807
+ ),
808
+ );
809
+ current();
810
+ const checks = value(
811
+ await client.checkObligations(taskId, signal),
812
+ );
813
+ current();
814
+ await show(
815
+ `Current changes (not attribution; bounded coverage)\n${pretty(changes)}\n\nConfigured obligations (not identical to recorded checks)\n${pretty(checks)}`,
816
+ );
817
+ continue;
818
+ }
819
+ if (action === "Plan / assignments") {
820
+ const expected = authority(view);
821
+ if (!view.readablePlan?.editable)
822
+ throw new ContractError("unavailable");
823
+ let assignments: Assignment[];
824
+ if (view.savedGraph.status === "saved") {
825
+ const graph = await history.graph(view.savedGraph.request);
826
+ if (!graph) continue;
827
+ assignments = graph.assignments;
828
+ } else {
829
+ const purpose = kind === "code" ? "edit" : kind;
830
+ if (!purpose) throw new ContractError("unavailable");
831
+ const role = view.roles.find(
832
+ (r) => r.role === roleForPurpose(purpose),
833
+ );
834
+ if (!role) throw new ContractError("unavailable");
835
+ assignments = [
836
+ {
837
+ id: "task",
838
+ purpose,
839
+ instructions:
840
+ "Perform the explicitly reviewed task and readable plan; report observations.",
841
+ expectedOutput: "Result and remaining uncertainty.",
842
+ role,
843
+ model: expected.model,
844
+ dependencies: [],
845
+ },
846
+ ];
847
+ }
848
+ const plan = await ctx.ui.editor(
849
+ "Readable plan • exact bytes, separate Save and Start",
850
+ view.readablePlan.text,
851
+ );
852
+ current();
853
+ if (plan === undefined) continue;
854
+ for (;;) {
855
+ const edit = await ui.select(
856
+ "Finite assignments (1–16), all required • default one explicit assignment",
857
+ [
858
+ "Preview assignments",
859
+ "Edit assignments / dependencies JSON",
860
+ "Save plan",
861
+ "Apply current exact model to assignments",
862
+ "Return",
863
+ ],
864
+ );
865
+ if (edit === "Preview assignments")
866
+ await show(pretty(assignments));
867
+ else if (edit === "Edit assignments / dependencies JSON") {
868
+ await show(
869
+ 'Edit the displayed finite assignments. Keep exact owner role hashes/model. Purpose cannot escalate readonly tasks. Dependencies: {"source":{"kind":"current","assignmentId":"predecessor"},"range":null,"requireCompleteObservation":false}. Historical selections must name actual retained run/child/result versions; ranges are UTF-8 bytes. Shared graph validation rejects cycles/missing/duplicate/oversized values. No generated output dispatches itself.',
870
+ );
871
+ const raw = await ctx.ui.editor(
872
+ "Assignments JSON • reviewed dependencies; no execution",
873
+ pretty(assignments),
874
+ );
875
+ current();
876
+ if (raw !== undefined) {
877
+ const parsed = parseWireJson(raw, LIMITS.graphBytes);
878
+ // proposePlan performs the single shared graph/role/model validation.
879
+ const proposal = await proposePlan(
880
+ view,
881
+ plan,
882
+ parsed as Assignment[],
883
+ {
884
+ uuid: randomUUID,
885
+ sha256: (bytes) =>
886
+ createHash("sha256").update(bytes).digest("hex"),
887
+ },
888
+ );
889
+ assignments = proposal.graph.assignments;
890
+ }
891
+ } else if (
892
+ edit === "Apply current exact model to assignments"
893
+ ) {
894
+ await fresh(expected);
895
+ if (
896
+ await ui.confirm(
897
+ "Use the current exact owner model for every assignment?",
898
+ pretty(expected.model),
899
+ )
900
+ )
901
+ assignments = assignments.map((a) => ({
902
+ ...a,
903
+ model: expected.model,
904
+ }));
905
+ } else if (edit === "Save plan") {
906
+ await fresh(expected);
907
+ const proposal = await proposePlan(view, plan, assignments, {
908
+ uuid: randomUUID,
909
+ sha256: (bytes) =>
910
+ createHash("sha256").update(bytes).digest("hex"),
911
+ });
912
+ current();
913
+ await submit(view, proposal);
914
+ break;
915
+ } else break;
916
+ }
917
+ } else if (
918
+ action === "Start / Continue" ||
919
+ action === "Generate plan (readonly)"
920
+ ) {
921
+ if (
922
+ action === "Start / Continue" &&
923
+ kind === "code" &&
924
+ !view.readablePlan?.text.trim()
925
+ ) {
926
+ ui.notify("Write and review a plan first.");
927
+ continue;
928
+ }
929
+ await submit(view, {
930
+ kind: action === "Start / Continue" ? "start" : "generate-plan",
931
+ taskId,
932
+ });
933
+ } else if (action === "Checks") {
934
+ const obligations = value(
935
+ await client.checkObligations(taskId, signal),
936
+ );
937
+ current();
938
+ // Configured names are data, even when named Return or a paging
939
+ // control. Keep a unique displayed index and map back by index.
940
+ const names = obligations.definitions.map(
941
+ (d, i) => `Configured ${i + 1} • ${d.selection.name}`,
942
+ );
943
+ const selected = await ui.select(
944
+ "Parent checks • model prose/tool exit is not PASS",
945
+ [...names, "Manual command", "Return"],
946
+ );
947
+ if (!selected || selected === "Return") continue;
948
+ let check =
949
+ obligations.definitions[names.indexOf(selected)]?.selection;
950
+ if (selected === "Manual command") {
951
+ await connection.refresh();
952
+ current();
953
+ if (!connection.session?.capabilities.manualChecks)
954
+ throw new ContractError("forbidden");
955
+ const command = await ui.input(
956
+ "Exact native-local shell command",
957
+ );
958
+ if (!command) continue;
959
+ const decoded = decodeAction({
960
+ kind: "check",
961
+ taskId,
962
+ check: {
963
+ kind: "manual",
964
+ name: "Manual",
965
+ command,
966
+ timeoutMs: 120000,
967
+ },
968
+ });
969
+ if (decoded.kind !== "check")
970
+ throw new ContractError("invalid");
971
+ check = decoded.check;
972
+ }
973
+ if (check) await submit(view, { kind: "check", taskId, check });
974
+ } else if (action === "Review / Done") {
975
+ const changes = value(
976
+ await client.currentChanges(
977
+ { storeId: session.storeId, taskId },
978
+ signal,
979
+ ),
980
+ );
981
+ current();
982
+ const checks = value(
983
+ await client.checkObligations(taskId, signal),
984
+ );
985
+ current();
986
+ const snapshot = value(
987
+ await client.snapshot({ kind: "task", id: taskId }, signal),
988
+ );
989
+ current();
990
+ await show(
991
+ `Current changes\n${pretty(changes)}\nConfigured obligations\n${pretty(checks)}\nNative/child results and prior human review are independent\n${pretty(snapshot)}`,
992
+ );
993
+ const runs = snapshot.runs.filter(
994
+ (r) => !r.childRunId && r.admission.kind !== "check",
995
+ );
996
+ const labels = runs.map(
997
+ (r) =>
998
+ `${r.runId} • ${r.admission.kind} • ${r.lifecycle}/${r.outcome ?? "unsettled"}`,
999
+ );
1000
+ const selected = await ui.select(
1001
+ "Select exact run for HUMAN review (never execution success)",
1002
+ [...labels, "Return"],
1003
+ );
1004
+ if (!selected || !labels.includes(selected)) continue;
1005
+ const acceptIssues = await ui.confirm(
1006
+ "Acknowledge unresolved issues?",
1007
+ "Failed, unrun, stale, incomplete checks and child/recording issues remain unresolved after human closure. No means only close if the owner finds no issues.",
1008
+ );
1009
+ await submit(view, {
1010
+ kind: "review",
1011
+ taskId,
1012
+ runId: runs[labels.indexOf(selected)].runId,
1013
+ changesIdentity: changes.observation.identity,
1014
+ checksRevision: checks.revision,
1015
+ acceptIssues,
1016
+ });
1017
+ }
1018
+ }
1019
+ }
1020
+ } catch (error) {
1021
+ ctx.ui.notify(
1022
+ `${error instanceof ContractError ? error.code : "unavailable"}: Cadence refused or observation unavailable. No automatic action replay.\n${attached ? "Reopen /cadence for a fresh observation and confirmation. Accepted work may continue. For quarantined/unavailable ownership use cadence-service diagnose --workspace <path>; no locks were cleared." : MANAGEMENT_GUIDANCE}`,
1023
+ "error",
1024
+ );
1025
+ } finally {
1026
+ latch.clear(); // no ownership of accepted work
1027
+ abort.abort();
1028
+ dismiss?.();
1029
+ try {
1030
+ await connection.detach();
1031
+ } catch {
1032
+ ctx.ui.notify(
1033
+ "Detach unconfirmed; exact attachment retained. Accepted work may continue. Explicit re-entry/management required.",
1034
+ "warning",
1035
+ );
1036
+ }
1037
+ modal = false;
1038
+ ctx.ui.notify(
1039
+ "Cadence detached locally. Accepted service work continues; only explicit Cancel requests stop.",
1040
+ "info",
1041
+ );
1042
+ }
1043
+ },
1044
+ });
1045
+ }