@abelxiaoxing/cadence 1.8.4 → 2.0.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 (202) hide show
  1. package/README.md +324 -117
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -18
  4. package/agents/diagnosis-worker.md +14 -14
  5. package/agents/implementation-worker.md +16 -17
  6. package/config/workflow-task.example.json +7 -0
  7. package/docs/remote-access.md +60 -0
  8. package/docs/reuse-and-distribution.md +44 -0
  9. package/docs/service-management.md +108 -0
  10. package/licenses/bundled-web.txt +6245 -0
  11. package/licenses/pi-web-ui-MIT.txt +21 -0
  12. package/licenses/pi-web-ui-reuse.md +22 -0
  13. package/package.json +92 -20
  14. package/src/index.ts +21 -1802
  15. package/src/native-build.ts +6 -0
  16. package/src/native-client.ts +103 -0
  17. package/src/native-export.ts +58 -0
  18. package/src/native-history.ts +359 -0
  19. package/src/operator-cli.mjs +453 -1937
  20. package/src/plan-proposal.ts +87 -0
  21. package/src/sdk-worker.mjs +549 -0
  22. package/src/service/contracts/active.ts +82 -0
  23. package/src/service/contracts/budget.ts +148 -0
  24. package/src/service/contracts/build.ts +67 -0
  25. package/src/service/contracts/capture.ts +48 -0
  26. package/src/service/contracts/core.ts +33 -0
  27. package/src/service/contracts/current.ts +261 -0
  28. package/src/service/contracts/domain.ts +68 -0
  29. package/src/service/contracts/history.ts +2245 -0
  30. package/src/service/contracts/models.ts +139 -0
  31. package/src/service/coordination.ts +862 -0
  32. package/src/service/private-files.ts +204 -0
  33. package/src/service-cli.mjs +5592 -0
  34. package/src/service-client.ts +747 -0
  35. package/src/service-contract.ts +245 -0
  36. package/src/service-host.mjs +22382 -0
  37. package/src/service-http-client.ts +425 -0
  38. package/src/service-wire.ts +372 -0
  39. package/src/task-shell.ts +1045 -0
  40. package/src/task-store.ts +479 -0
  41. package/src/task-types.ts +184 -0
  42. package/src/verification-diagnostics.ts +29 -72
  43. package/src/worker-capture.mjs +1197 -0
  44. package/src/worker-public.mjs +1753 -0
  45. package/src/worker-redaction.mjs +608 -0
  46. package/src/worker-tools.mjs +202 -0
  47. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  50. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  56. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  71. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  74. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  92. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  104. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  107. package/web/dist/assets/cadence.css +2 -0
  108. package/web/dist/assets/cadence.js +7 -0
  109. package/web/dist/assets/markdown.js +296 -0
  110. package/web/dist/assets/react.js +9 -0
  111. package/web/dist/assets/rolldown-runtime.js +1 -0
  112. package/web/dist/assets/timeline-client.js +41 -0
  113. package/web/dist/assets/vis-timeline.css +1274 -0
  114. package/web/dist/assets/vis-timeline.js +32 -0
  115. package/web/dist/index.html +18 -0
  116. package/web/dist/manifest.json +505 -0
  117. package/config/plan-draft.example.json +0 -125
  118. package/config/plan-draft.multiple-tasks.example.json +0 -375
  119. package/config/plan-draft.quick.example.json +0 -78
  120. package/prompts/abel-design.md +0 -382
  121. package/prompts/abel-diagnose.md +0 -71
  122. package/prompts/abel-implement.md +0 -284
  123. package/prompts/abel-init.md +0 -52
  124. package/src/activation.ts +0 -44
  125. package/src/activity-contracts.ts +0 -82
  126. package/src/agent-registry.ts +0 -53
  127. package/src/apply-transaction.ts +0 -1437
  128. package/src/artifact-store.ts +0 -213
  129. package/src/build-windows-job.mjs +0 -95
  130. package/src/candidate-artifact.ts +0 -143
  131. package/src/candidate-context.ts +0 -289
  132. package/src/candidate-patch.ts +0 -271
  133. package/src/candidate-workspace.ts +0 -80
  134. package/src/canonical.ts +0 -16
  135. package/src/change-contract-schema.ts +0 -191
  136. package/src/change-contract.ts +0 -324
  137. package/src/change-verification.ts +0 -1617
  138. package/src/contracts.ts +0 -2466
  139. package/src/control-contracts.ts +0 -382
  140. package/src/delivery-compiler.ts +0 -1805
  141. package/src/delivery-revision.ts +0 -42
  142. package/src/design-control.ts +0 -1299
  143. package/src/design-diagnostics.ts +0 -181
  144. package/src/design-journal.ts +0 -1773
  145. package/src/directory-sync.ts +0 -36
  146. package/src/durable-contracts.ts +0 -227
  147. package/src/durable-workflow.ts +0 -822
  148. package/src/evidence-draft.ts +0 -122
  149. package/src/execution-profile.ts +0 -64
  150. package/src/execution-retention.ts +0 -77
  151. package/src/file-snapshot.ts +0 -163
  152. package/src/implement-continuation.ts +0 -559
  153. package/src/implement-graph.ts +0 -690
  154. package/src/implement-plan.ts +0 -72
  155. package/src/install-config.d.mts +0 -6
  156. package/src/isolation-backend.ts +0 -353
  157. package/src/model-source.ts +0 -22
  158. package/src/openspec-cli.ts +0 -494
  159. package/src/operator-cli-entry.mjs +0 -24
  160. package/src/operator-tools.ts +0 -309
  161. package/src/package-candidate-process.ts +0 -206
  162. package/src/package-candidate.ts +0 -24
  163. package/src/package-delivery.ts +0 -486
  164. package/src/package-state.ts +0 -320
  165. package/src/package-verification.ts +0 -635
  166. package/src/package-workflow.ts +0 -305
  167. package/src/packet-runtime.ts +0 -459
  168. package/src/phase-execution.ts +0 -2163
  169. package/src/pi-adapter.ts +0 -19
  170. package/src/plan-draft-summary.ts +0 -527
  171. package/src/plan-draft.ts +0 -502
  172. package/src/remove-path.ts +0 -32
  173. package/src/run-state.ts +0 -420
  174. package/src/run-store.ts +0 -1179
  175. package/src/safe-path.ts +0 -77
  176. package/src/scoped-grep-worker.mjs +0 -23
  177. package/src/scoped-tools.ts +0 -775
  178. package/src/single-task-draft.ts +0 -142
  179. package/src/sqlite-schema.ts +0 -171
  180. package/src/state-root.ts +0 -215
  181. package/src/storage-schema.ts +0 -360
  182. package/src/subagent-activity.ts +0 -1417
  183. package/src/subagent-process.ts +0 -476
  184. package/src/task-ledger.ts +0 -1338
  185. package/src/transport-budget.ts +0 -50
  186. package/src/verification-capability.ts +0 -1191
  187. package/src/verification-environment-io.ts +0 -389
  188. package/src/verification-environment.ts +0 -162
  189. package/src/verification-identity.ts +0 -77
  190. package/src/verification-scan.ts +0 -64
  191. package/src/windows-job-backend.ts +0 -419
  192. package/src/windows-job.c +0 -149
  193. package/src/workflow-engine.ts +0 -28
  194. package/src/workflow-policy.ts +0 -1670
  195. package/src/workflow-recovery-policy.ts +0 -97
  196. package/src/workflow-scheduling.ts +0 -105
  197. package/src/workflow-state-machine.ts +0 -4341
  198. package/src/workflow-status.ts +0 -576
  199. package/src/workspace-io-entry.mjs +0 -76
  200. package/src/workspace-io-worker.mjs +0 -1312
  201. package/src/workspace-io.ts +0 -89
  202. 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
+ }