@b00dea/cockpit 0.1.0 → 0.1.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 (196) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +3 -0
  3. package/cockpit.schema.json +73 -3
  4. package/dist/autonomy.d.ts +3 -2
  5. package/dist/autonomy.js +30 -13
  6. package/dist/browser/commands.d.ts +57 -2
  7. package/dist/browser/commands.js +114 -10
  8. package/dist/browser/runtime.d.ts +1 -0
  9. package/dist/browser/runtime.js +4 -1
  10. package/dist/browser/surfaces.d.ts +7 -7
  11. package/dist/browser/surfaces.js +5 -0
  12. package/dist/canonical-json.d.ts +2 -0
  13. package/dist/canonical-json.js +5 -0
  14. package/dist/cli-main.d.ts +40 -3
  15. package/dist/cli-main.js +241 -62
  16. package/dist/command-table.d.ts +19 -11
  17. package/dist/command-table.js +190 -38
  18. package/dist/config/load.d.ts +16 -4
  19. package/dist/config/load.js +25 -3
  20. package/dist/config/unconfirmed-toml.d.ts +9 -0
  21. package/dist/config/unconfirmed-toml.js +29 -0
  22. package/dist/diagnostics.js +10 -1
  23. package/dist/docs/check.d.ts +16 -0
  24. package/dist/docs/check.js +197 -0
  25. package/dist/docs/decision-record.d.ts +34 -0
  26. package/dist/docs/decision-record.js +135 -0
  27. package/dist/docs/decisions.d.ts +16 -0
  28. package/dist/docs/decisions.js +103 -0
  29. package/dist/docs/markdown.d.ts +36 -0
  30. package/dist/docs/markdown.js +88 -0
  31. package/dist/docs/references.d.ts +32 -0
  32. package/dist/docs/references.js +148 -0
  33. package/dist/environment/application/confirm.d.ts +35 -0
  34. package/dist/environment/application/confirm.js +122 -0
  35. package/dist/environment/application/destructive.js +6 -2
  36. package/dist/environment/application/doctor.d.ts +12 -7
  37. package/dist/environment/application/doctor.js +20 -8
  38. package/dist/environment/application/guard.d.ts +12 -6
  39. package/dist/environment/application/guard.js +87 -32
  40. package/dist/environment/application/health.d.ts +14 -5
  41. package/dist/environment/application/health.js +8 -2
  42. package/dist/environment/application/lifecycle-context.d.ts +10 -3
  43. package/dist/environment/application/lifecycle-state.js +4 -3
  44. package/dist/environment/application/lifecycle.d.ts +2 -1
  45. package/dist/environment/application/lifecycle.js +11 -3
  46. package/dist/environment/application/services.d.ts +60 -0
  47. package/dist/environment/application/services.js +89 -0
  48. package/dist/environment/application/siblings.d.ts +43 -0
  49. package/dist/environment/application/siblings.js +269 -0
  50. package/dist/environment/claims.d.ts +10 -10
  51. package/dist/environment/claims.js +3 -11
  52. package/dist/environment/compose-topology.d.ts +15 -7
  53. package/dist/environment/compose-topology.js +100 -23
  54. package/dist/environment/contract.d.ts +42 -9
  55. package/dist/environment/contract.js +33 -0
  56. package/dist/environment/executable.d.ts +12 -0
  57. package/dist/environment/executable.js +46 -23
  58. package/dist/environment/generated-application/check.js +2 -0
  59. package/dist/environment/generated-application/prepare.js +2 -0
  60. package/dist/environment/generated-application/shared.d.ts +10 -2
  61. package/dist/environment/generated-application/shared.js +6 -1
  62. package/dist/environment/generated-application/surface-outputs.js +23 -12
  63. package/dist/environment/generated-application/surfaces.js +16 -11
  64. package/dist/environment/generated-application/sync.js +19 -4
  65. package/dist/environment/git-observation.d.ts +25 -6
  66. package/dist/environment/git-observation.js +37 -28
  67. package/dist/environment/health-composition.d.ts +3 -2
  68. package/dist/environment/health-composition.js +19 -2
  69. package/dist/environment/init-application.d.ts +39 -14
  70. package/dist/environment/init-application.js +39 -10
  71. package/dist/environment/lifecycle-composition.d.ts +4 -2
  72. package/dist/environment/lifecycle-composition.js +84 -17
  73. package/dist/environment/lifecycle-stage.d.ts +8 -3
  74. package/dist/environment/lifecycle-stage.js +11 -1
  75. package/dist/environment/logs.d.ts +1 -1
  76. package/dist/environment/map-limit.d.ts +2 -0
  77. package/dist/environment/map-limit.js +13 -0
  78. package/dist/environment/mcp-observation.d.ts +9 -0
  79. package/dist/environment/mcp-observation.js +46 -0
  80. package/dist/environment/observations.d.ts +1 -1
  81. package/dist/environment/observations.js +2 -0
  82. package/dist/environment/operator-application.d.ts +35 -6
  83. package/dist/environment/operator-application.js +42 -0
  84. package/dist/environment/presence.d.ts +4 -0
  85. package/dist/environment/presence.js +26 -0
  86. package/dist/environment/read-composition.d.ts +8 -3
  87. package/dist/environment/read-composition.js +53 -8
  88. package/dist/environment/readiness-composition.d.ts +20 -1
  89. package/dist/environment/readiness-composition.js +23 -2
  90. package/dist/environment/reclaim.d.ts +56 -19
  91. package/dist/environment/reclaim.js +147 -42
  92. package/dist/environment/runtime.d.ts +64 -18
  93. package/dist/environment/runtime.js +73 -3
  94. package/dist/environment/served-checkout-grants.d.ts +53 -0
  95. package/dist/environment/served-checkout-grants.js +182 -0
  96. package/dist/environment/sibling-checkouts.d.ts +119 -0
  97. package/dist/environment/sibling-checkouts.js +254 -0
  98. package/dist/environment/sibling-observation.d.ts +27 -0
  99. package/dist/environment/sibling-observation.js +45 -0
  100. package/dist/environment/sibling-pull.d.ts +23 -0
  101. package/dist/environment/sibling-pull.js +95 -0
  102. package/dist/environment/stack-inputs.d.ts +18 -0
  103. package/dist/environment/stack-inputs.js +210 -0
  104. package/dist/environment/stack-inventory-prune.d.ts +31 -0
  105. package/dist/environment/stack-inventory-prune.js +129 -0
  106. package/dist/environment/stack-inventory-report.d.ts +44 -0
  107. package/dist/environment/stack-inventory-report.js +78 -0
  108. package/dist/environment/stack-inventory.d.ts +94 -0
  109. package/dist/environment/stack-inventory.js +418 -0
  110. package/dist/environment/state.d.ts +48 -9
  111. package/dist/environment/state.js +142 -2
  112. package/dist/environment/tool-registration.js +3 -7
  113. package/dist/environment-exec/execute.d.ts +6 -3
  114. package/dist/environment-exec/execute.js +4 -0
  115. package/dist/fix-catalogue.d.ts +1 -1
  116. package/dist/generated-environment/adopt.js +4 -8
  117. package/dist/guards/answer-confirmation.d.ts +41 -0
  118. package/dist/guards/answer-confirmation.js +109 -0
  119. package/dist/guards/evaluator.d.ts +20 -3
  120. package/dist/guards/evaluator.js +39 -7
  121. package/dist/guards/local-identity.d.ts +6 -2
  122. package/dist/guards/local-identity.js +10 -4
  123. package/dist/guards/policy-change.d.ts +43 -0
  124. package/dist/guards/policy-change.js +159 -0
  125. package/dist/guards/policy-pin.d.ts +1 -1
  126. package/dist/guards/policy-pin.js +14 -12
  127. package/dist/init/command.d.ts +4 -3
  128. package/dist/init/command.js +36 -16
  129. package/dist/init/derive.d.ts +8 -6
  130. package/dist/init/derive.js +140 -36
  131. package/dist/init/draft.d.ts +13 -11
  132. package/dist/init/draft.js +54 -31
  133. package/dist/init/render.d.ts +18 -6
  134. package/dist/init/render.js +31 -9
  135. package/dist/repository-path.d.ts +9 -0
  136. package/dist/repository-path.js +16 -0
  137. package/dist/schema.d.ts +42 -75
  138. package/dist/schema.js +62 -11
  139. package/dist/surfaces/harness-registry.d.ts +80 -0
  140. package/dist/surfaces/harness-registry.js +29 -0
  141. package/dist/surfaces/mcp.d.ts +67 -17
  142. package/dist/surfaces/mcp.js +194 -31
  143. package/dist/surfaces/projection.d.ts +7 -7
  144. package/dist/surfaces/projection.js +41 -6
  145. package/dist/surfaces/render.d.ts +5 -44
  146. package/dist/surfaces/render.js +5 -2
  147. package/dist/team-metadata.js +2 -1
  148. package/dist/testing/test-protection.d.ts +34 -0
  149. package/dist/testing/test-protection.js +137 -0
  150. package/dist/testing/weakening.d.ts +15 -0
  151. package/dist/testing/weakening.js +30 -0
  152. package/dist/work-orders/application-closure.d.ts +7 -0
  153. package/dist/work-orders/application-closure.js +76 -0
  154. package/dist/work-orders/application-context.d.ts +162 -0
  155. package/dist/work-orders/application-context.js +137 -0
  156. package/dist/work-orders/application-decisions.d.ts +27 -0
  157. package/dist/work-orders/application-decisions.js +121 -0
  158. package/dist/work-orders/application-grading.d.ts +11 -0
  159. package/dist/work-orders/application-grading.js +212 -0
  160. package/dist/work-orders/application.d.ts +4 -97
  161. package/dist/work-orders/application.js +94 -254
  162. package/dist/work-orders/composition.d.ts +35 -58
  163. package/dist/work-orders/composition.js +216 -24
  164. package/dist/work-orders/contract.d.ts +22 -6
  165. package/dist/work-orders/contract.js +34 -16
  166. package/dist/work-orders/decisions.d.ts +23 -0
  167. package/dist/work-orders/decisions.js +55 -0
  168. package/dist/work-orders/store.d.ts +54 -5
  169. package/dist/work-orders/store.js +118 -24
  170. package/docs/README.md +6 -3
  171. package/docs/capability-matrix.md +17 -9
  172. package/docs/cli-reference.md +209 -1
  173. package/docs/command-list.md +12 -1
  174. package/docs/concepts.md +6 -4
  175. package/docs/configuration-essentials.md +7 -2
  176. package/docs/configuration-reference.md +18 -3
  177. package/docs/docs-check.md +76 -0
  178. package/docs/environment.md +194 -6
  179. package/docs/faq.md +5 -3
  180. package/docs/getting-started.md +109 -25
  181. package/docs/harness-surfaces.md +69 -2
  182. package/docs/safety-and-autonomy.md +56 -12
  183. package/docs/troubleshooting.md +29 -8
  184. package/docs/tutorial.md +3 -2
  185. package/docs/work-and-browser.md +180 -14
  186. package/libexec/browser-operation.cjs +86 -2
  187. package/package.json +2 -2
  188. package/surfaces/skills/cockpit-browser.skill.md +7 -2
  189. package/surfaces/skills/cockpit-explain.skill.md +1 -1
  190. package/surfaces/skills/cockpit-fix.skill.md +1 -1
  191. package/surfaces/skills/cockpit-setup.skill.md +3 -3
  192. package/surfaces/skills/cockpit-work.skill.md +1 -1
  193. package/surfaces/skills/procedures/explain.md +9 -4
  194. package/surfaces/skills/procedures/fix.md +25 -11
  195. package/surfaces/skills/procedures/setup.md +113 -40
  196. package/surfaces/skills/procedures/work.md +45 -16
@@ -0,0 +1,162 @@
1
+ import { z } from 'zod';
2
+ import { type WorkGate } from '../autonomy.js';
3
+ import type { Decider } from '../schema.js';
4
+ import { type WorkOrder } from './contract.js';
5
+ import type { CheckResult, WorkRecord, WorkStore } from './store.js';
6
+ import type { WorkApplicationComposition } from './composition.js';
7
+ declare const Request: z.ZodDiscriminatedUnion<[z.ZodObject<{
8
+ operation: z.ZodLiteral<"work-status">;
9
+ workOrderId: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strict>, z.ZodObject<{
11
+ operation: z.ZodLiteral<"work-accept">;
12
+ workOrderId: z.ZodString;
13
+ }, z.core.$strict>, z.ZodObject<{
14
+ operation: z.ZodLiteral<"work-check">;
15
+ workOrderId: z.ZodString;
16
+ }, z.core.$strict>, z.ZodObject<{
17
+ operation: z.ZodLiteral<"work-note">;
18
+ workOrderId: z.ZodString;
19
+ text: z.ZodString;
20
+ kind: z.ZodEnum<{
21
+ decision: "decision";
22
+ note: "note";
23
+ question: "question";
24
+ }>;
25
+ }, z.core.$strict>, z.ZodObject<{
26
+ operation: z.ZodLiteral<"work-decide">;
27
+ workOrderId: z.ZodString;
28
+ decision: z.ZodNumber;
29
+ resolution: z.ZodDiscriminatedUnion<[z.ZodObject<{
30
+ kind: z.ZodLiteral<"record">;
31
+ path: z.ZodOptional<z.ZodString>;
32
+ supersedes: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strict>, z.ZodObject<{
34
+ kind: z.ZodLiteral<"none">;
35
+ reason: z.ZodString;
36
+ }, z.core.$strict>], "kind">;
37
+ }, z.core.$strict>, z.ZodObject<{
38
+ operation: z.ZodLiteral<"work-answer">;
39
+ workOrderId: z.ZodString;
40
+ question: z.ZodNumber;
41
+ text: z.ZodString;
42
+ }, z.core.$strict>, z.ZodObject<{
43
+ operation: z.ZodLiteral<"work-grade">;
44
+ workOrderId: z.ZodString;
45
+ verdict: z.ZodOptional<z.ZodEnum<{
46
+ met: "met";
47
+ unmet: "unmet";
48
+ }>>;
49
+ note: z.ZodOptional<z.ZodString>;
50
+ }, z.core.$strict>, z.ZodObject<{
51
+ operation: z.ZodLiteral<"work-signoff">;
52
+ workOrderId: z.ZodString;
53
+ close: z.ZodEnum<{
54
+ abandoned: "abandoned";
55
+ done: "done";
56
+ partial: "partial";
57
+ }>;
58
+ note: z.ZodOptional<z.ZodString>;
59
+ }, z.core.$strict>, z.ZodObject<{
60
+ operation: z.ZodLiteral<"work-acknowledge">;
61
+ workOrderId: z.ZodString;
62
+ note: z.ZodString;
63
+ }, z.core.$strict>, z.ZodObject<{
64
+ operation: z.ZodLiteral<"work-recover">;
65
+ workOrderId: z.ZodString;
66
+ apply: z.ZodBoolean;
67
+ }, z.core.$strict>, z.ZodObject<{
68
+ operation: z.ZodLiteral<"work-verify">;
69
+ }, z.core.$strict>, z.ZodObject<{
70
+ operation: z.ZodLiteral<"grade-criteria">;
71
+ criteriaFile: z.ZodString;
72
+ }, z.core.$strict>, z.ZodObject<{
73
+ operation: z.ZodEnum<{
74
+ "browser-drive": "browser-drive";
75
+ "browser-screenshot": "browser-screenshot";
76
+ }>;
77
+ role: z.ZodOptional<z.ZodString>;
78
+ route: z.ZodOptional<z.ZodString>;
79
+ failOnFindings: z.ZodOptional<z.ZodBoolean>;
80
+ }, z.core.$strict>, z.ZodObject<{
81
+ operation: z.ZodLiteral<"test-run">;
82
+ gate: z.ZodString;
83
+ mode: z.ZodEnum<{
84
+ canonical: "canonical";
85
+ full: "full";
86
+ staged: "staged";
87
+ }>;
88
+ select: z.ZodOptional<z.ZodObject<{
89
+ kind: z.ZodEnum<{
90
+ file: "file";
91
+ route: "route";
92
+ spec: "spec";
93
+ test: "test";
94
+ }>;
95
+ value: z.ZodString;
96
+ }, z.core.$strict>>;
97
+ }, z.core.$strict>], "operation">;
98
+ export type WorkApplicationRequest = z.infer<typeof Request>;
99
+ export type Operation = WorkApplicationRequest['operation'];
100
+ export declare function captureWorkApplicationRequest(value: unknown): WorkApplicationRequest | undefined;
101
+ interface WorkApplicationContinuation {
102
+ readonly digest: string;
103
+ readonly prompt: string;
104
+ readonly decide: (answer: string) => Promise<WorkApplicationOutcome>;
105
+ }
106
+ export type WorkApplicationOutcome = Readonly<{
107
+ kind: 'complete';
108
+ operation: Operation;
109
+ value: unknown;
110
+ }> | Readonly<{
111
+ kind: 'human-needed';
112
+ operation: Operation;
113
+ continuation: WorkApplicationContinuation;
114
+ }> | Readonly<{
115
+ kind: 'refusal';
116
+ operation: Operation | 'invalid';
117
+ reason: string;
118
+ }> | Readonly<{
119
+ kind: 'unsatisfied';
120
+ operation: Operation;
121
+ value: unknown;
122
+ reason: string;
123
+ weakening?: string;
124
+ }>;
125
+ export declare const refusal: (operation: Operation | 'invalid', reason: string) => WorkApplicationOutcome;
126
+ export declare const complete: (operation: Operation, value: unknown) => WorkApplicationOutcome;
127
+ export declare const unsatisfied: (operation: Operation, value: unknown, reason: string, weakening?: string) => WorkApplicationOutcome;
128
+ export declare const now: () => string;
129
+ export declare const TERMINAL: Set<"abandoned" | "accepted" | "checked" | "done" | "graded" | "partial">;
130
+ export type Observed = Readonly<{
131
+ id: string;
132
+ order?: WorkOrder;
133
+ problems: readonly string[];
134
+ record?: WorkRecord;
135
+ digest?: string;
136
+ }>;
137
+ export declare function displayState(row: Observed, attemptCap: number): string;
138
+ export declare function openQuestions(record: WorkRecord): readonly number[];
139
+ export type WorkRequest = Exclude<WorkApplicationRequest, {
140
+ operation: 'test-run' | `browser-${string}`;
141
+ }>;
142
+ export interface WorkContext {
143
+ readonly composition: WorkApplicationComposition;
144
+ readonly operation: WorkRequest['operation'];
145
+ readonly policy: WorkApplicationComposition['policy'];
146
+ readonly store: WorkStore;
147
+ readonly observe: (orderId: string) => Promise<Observed>;
148
+ /** How many accepted orders are not closed. */
149
+ readonly wip: () => Promise<number>;
150
+ /** The records under [docs] decisions, read once per command. */
151
+ readonly decisionRecords: () => Promise<ReadonlyMap<string, string>>;
152
+ readonly gated: (gate: WorkGate, decider: Decider, lines: readonly string[], verb: string, apply: (by: Decider) => Promise<WorkApplicationOutcome>) => Promise<WorkApplicationOutcome>;
153
+ /** The order and its record, refused unless it is accepted, open, unchanged and not blocked. */
154
+ readonly current: (orderId: string) => Promise<Readonly<{
155
+ row: Observed;
156
+ record: WorkRecord;
157
+ order: WorkOrder;
158
+ }>>;
159
+ readonly runChecks: (order: Pick<WorkOrder, 'criteria' | 'gates'>) => Promise<readonly CheckResult[]>;
160
+ }
161
+ export declare function workContext(composition: WorkApplicationComposition, operation: WorkRequest['operation']): WorkContext;
162
+ export {};
@@ -0,0 +1,137 @@
1
+ // What every work command shares: the request it takes, the outcome it returns, the state it reads an
2
+ // order in, and the steps several commands take (observing an order, passing a gate, running its
3
+ // checks), bound to one composition.
4
+ import { createHash } from 'node:crypto';
5
+ import { z } from 'zod';
6
+ import { decide } from '../autonomy.js';
7
+ import { RefusalError } from '../environment/contract.js';
8
+ import { REPOSITORY_FILE } from '../repository-path.js';
9
+ import { WORK_ORDER_ID, workOrderDigest } from './contract.js';
10
+ const id = z.string().regex(WORK_ORDER_ID);
11
+ const note = z.string().trim().min(1).max(4_096).regex(/^[^\x00-\x08\x0b-\x1f\x7f]*$/);
12
+ const docsReference = z.string().max(1_024).regex(REPOSITORY_FILE);
13
+ const relativeFile = z.string().max(4_096).regex(/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._@+\-/]+$/);
14
+ const Request = z.discriminatedUnion('operation', [
15
+ z.object({ operation: z.literal('work-status'), workOrderId: id.optional() }).strict(),
16
+ z.object({ operation: z.literal('work-accept'), workOrderId: id }).strict(),
17
+ z.object({ operation: z.literal('work-check'), workOrderId: id }).strict(),
18
+ z.object({ operation: z.literal('work-note'), workOrderId: id, text: note, kind: z.enum(['note', 'question', 'decision']) }).strict(),
19
+ z.object({ operation: z.literal('work-decide'), workOrderId: id, decision: z.number().int().min(1), resolution: z.discriminatedUnion('kind', [
20
+ z.object({ kind: z.literal('record'), path: docsReference.optional(), supersedes: z.string().regex(/^\d{1,9}$/).optional() }).strict(),
21
+ z.object({ kind: z.literal('none'), reason: note }).strict(),
22
+ ]) }).strict(),
23
+ z.object({ operation: z.literal('work-answer'), workOrderId: id, question: z.number().int().min(1), text: note }).strict(),
24
+ z.object({ operation: z.literal('work-grade'), workOrderId: id, verdict: z.enum(['met', 'unmet']).optional(), note: note.optional() }).strict(),
25
+ z.object({ operation: z.literal('work-signoff'), workOrderId: id, close: z.enum(['done', 'partial', 'abandoned']), note: note.optional() }).strict(),
26
+ z.object({ operation: z.literal('work-acknowledge'), workOrderId: id, note }).strict(),
27
+ z.object({ operation: z.literal('work-recover'), workOrderId: id, apply: z.boolean() }).strict(),
28
+ z.object({ operation: z.literal('work-verify') }).strict(),
29
+ z.object({ operation: z.literal('grade-criteria'), criteriaFile: relativeFile }).strict(),
30
+ z.object({ operation: z.enum(['browser-drive', 'browser-screenshot']), role: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/).optional(),
31
+ route: z.string().regex(/^(?:[a-z0-9][a-z0-9_-]{0,63}|\/(?!\/)[^\s#\x00-\x1f\x7f]{0,511})$/).optional(),
32
+ failOnFindings: z.boolean().optional() }).strict(),
33
+ z.object({ operation: z.literal('test-run'), gate: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/), mode: z.enum(['canonical', 'full', 'staged']),
34
+ select: z.object({ kind: z.enum(['spec', 'file', 'test', 'route']), value: z.string().min(1).refine(value => Buffer.byteLength(value) <= 4_096 && !/[\x00-\x1f\x7f]/.test(value)) }).strict().optional() }).strict(),
35
+ ]);
36
+ export function captureWorkApplicationRequest(value) {
37
+ const parsed = Request.safeParse(value);
38
+ return parsed.success ? parsed.data : undefined;
39
+ }
40
+ export const refusal = (operation, reason) => ({ kind: 'refusal', operation, reason });
41
+ export const complete = (operation, value) => ({ kind: 'complete', operation, value });
42
+ export const unsatisfied = (operation, value, reason, weakening) => ({ kind: 'unsatisfied', operation, value, reason, ...weakening ? { weakening } : {} });
43
+ export const now = () => new Date().toISOString();
44
+ // Agent-written text is shown to a person with its control and bidirectional-override characters
45
+ // replaced, so it can neither drive their terminal nor read in an order other than the one recorded.
46
+ const shown = (value) => value.replace(/[\x00-\x09\x0b-\x1f\x7f-\x9f‪-‮⁦-⁩]/g, '�');
47
+ export const TERMINAL = new Set(['done', 'partial', 'abandoned']);
48
+ export function displayState(row, attemptCap) {
49
+ if (!row.record)
50
+ return row.order ? 'draft' : 'invalid';
51
+ if (TERMINAL.has(row.record.state))
52
+ return row.record.state;
53
+ if (!row.order || row.digest !== row.record.accepted.digest)
54
+ return 'stale';
55
+ if (openQuestions(row.record).length || row.record.attempts >= attemptCap)
56
+ return 'blocked';
57
+ return row.record.state;
58
+ }
59
+ export function openQuestions(record) {
60
+ return record.notes.flatMap((row, index) => row.kind === 'question' && !record.notes.some(answer => answer.answers === index + 1) ? [index + 1] : []);
61
+ }
62
+ export function workContext(composition, operation) {
63
+ const { policy, store } = composition;
64
+ const observe = async (orderId) => {
65
+ const [parsed, record] = await Promise.all([composition.readOrder(orderId), store.read(orderId)]);
66
+ if (!parsed.ok)
67
+ return { id: orderId, problems: parsed.problems, ...(record ? { record } : {}) };
68
+ // A source that cannot be read leaves this order without a digest, and so stale, not the whole listing refused.
69
+ let sources;
70
+ try {
71
+ sources = await composition.sourceDigests(parsed.value);
72
+ }
73
+ catch (error) {
74
+ if (!(error instanceof RefusalError))
75
+ throw error;
76
+ return { id: orderId, order: parsed.value, problems: [`a cited source cannot be read: ${error.message}`], ...(record ? { record } : {}) };
77
+ }
78
+ const base = record?.accepted.base ?? await composition.heads();
79
+ return { id: orderId, order: parsed.value, problems: [], ...(record ? { record } : {}), digest: workOrderDigest(parsed.value, sources, base) };
80
+ };
81
+ const wip = async () => (await store.list()).filter(record => !TERMINAL.has(record.state)).length;
82
+ let records;
83
+ const decisionRecords = () => records ??= composition.decisionRecords();
84
+ // Who passed a gate: the agent; a person, after typing the digest of exactly what they were shown;
85
+ // or CI, which locally records the step as awaiting the `work verify` job and passes it there.
86
+ const gated = async (gate, decider, lines, verb, apply) => {
87
+ const decision = decide(decider, composition.env);
88
+ if (decision === 'proceed')
89
+ return apply(decider === 'ci' ? 'ci' : 'agent');
90
+ if (decision === 'ci-only')
91
+ return apply('ci');
92
+ const digest = createHash('sha256').update(JSON.stringify({ gate, lines })).digest('hex');
93
+ let used = false;
94
+ return { kind: 'human-needed', operation, continuation: { digest,
95
+ prompt: [...lines.map(shown), `[work.gates] ${gate} = person. To ${verb} exactly this, type ${verb} ${digest}`].join('\n'),
96
+ decide: async (answer) => {
97
+ if (used)
98
+ return refusal(operation, `this prompt was already answered; rerun work ${operation.slice('work-'.length)}`);
99
+ used = true;
100
+ return answer.trim() === `${verb} ${digest}` ? apply('person') : refusal(operation, `Nothing was recorded: the person did not type ${verb} ${digest}`);
101
+ } } };
102
+ };
103
+ // Who closes or amends a capped order is whoever [work.gates] says; a sentence naming a person where the policy names the agent is a control that is not there.
104
+ const attemptCapRemedy = (orderId) => {
105
+ const by = (gate, verb, command) => policy.gates[gate] === 'person' ? `a person ${verb} it with ${command} --interactive`
106
+ : `${verb.replace(/s$/, '')} it with ${command}${policy.gates[gate] === 'ci' ? ', which the work verify job confirms in CI' : ''}`;
107
+ return `${by('signoff', 'closes', `npx --no cockpit work signoff --config <your cockpit.toml> --work-order ${orderId} --partial|--abandon --note <why>`)}, `
108
+ + `or ${by('amend', 'amends', `an edit to workorders/${orderId}.md and npx --no cockpit work accept --config <your cockpit.toml> --work-order ${orderId}`)}`;
109
+ };
110
+ const current = async (orderId) => {
111
+ const row = await observe(orderId);
112
+ if (!row.record)
113
+ throw new RefusalError(`${orderId} is not accepted yet; run npx --no cockpit work accept --config <your cockpit.toml> --work-order ${orderId}`);
114
+ const state = displayState(row, composition.caps.attempts);
115
+ if (TERMINAL.has(row.record.state))
116
+ throw new RefusalError(`${orderId} is closed (${row.record.state})`);
117
+ if (state === 'stale')
118
+ throw new RefusalError(`${orderId} changed since it was accepted${row.problems.length ? ` and does not parse: ${row.problems.join('; ')}` : ''}; `
119
+ + `run npx --no cockpit work accept --config <your cockpit.toml> --work-order ${orderId} to accept the change`);
120
+ if (state === 'blocked')
121
+ throw new RefusalError(openQuestions(row.record).length
122
+ ? `${orderId} has open question ${openQuestions(row.record).join(', ')}; it is answered with work answer`
123
+ : `${orderId} reached the attempt cap (${composition.caps.attempts}); record the root cause with work note, then ${attemptCapRemedy(orderId)}`);
124
+ return { row, record: row.record, order: row.order };
125
+ };
126
+ const runChecks = async (order) => {
127
+ const results = [];
128
+ // Checks share the product's state, so they run in the order the contract lists them.
129
+ for (const criterion of order.criteria)
130
+ if (criterion.check)
131
+ results.push(await composition.runCheck(criterion.id, criterion.check));
132
+ for (const gate of order.gates)
133
+ results.push(await composition.runCheck(`gate:${gate}`, { gate }));
134
+ return results;
135
+ };
136
+ return { composition, operation, policy, store, observe, wip, decisionRecords, gated, current, runChecks };
137
+ }
@@ -0,0 +1,27 @@
1
+ import type { WorkOrder } from './contract.js';
2
+ import type { WorkRecord } from './store.js';
3
+ import type { WorkApplicationComposition } from './composition.js';
4
+ import { type WorkApplicationOutcome, type WorkContext, type WorkRequest } from './application-context.js';
5
+ export declare const DECISIONS_UNDECLARED: string;
6
+ export declare function decisions(record: WorkRecord): {
7
+ decision: number;
8
+ title: string;
9
+ resolution: {
10
+ at: string;
11
+ by: "agent" | "ci" | "person";
12
+ kind: "answer" | "decision" | "note" | "question" | "resolution";
13
+ text: string;
14
+ answers?: number | undefined;
15
+ resolves?: number | undefined;
16
+ record?: string | undefined;
17
+ } | undefined;
18
+ }[];
19
+ export declare function recordProblem(text: string | undefined, reference: string, orderId: string, filled: boolean): string | undefined;
20
+ export declare function unresolvedDecisions(record: WorkRecord, composition: WorkApplicationComposition): Promise<readonly string[]>;
21
+ export declare function relatedDecisions(context: WorkContext, order: WorkOrder): Promise<Readonly<{
22
+ relatedDecisions?: readonly string[];
23
+ relatedDecisionsUnavailable?: string;
24
+ }>>;
25
+ export declare function decideOrder(request: Extract<WorkRequest, {
26
+ operation: 'work-decide';
27
+ }>, context: WorkContext): Promise<WorkApplicationOutcome>;
@@ -0,0 +1,121 @@
1
+ // The decisions an order records: which are unresolved, whether a cited record still stands for its
2
+ // decision, the records that bear on an order's scope, and `work decide`, which writes or cites one.
3
+ import { posix } from 'node:path';
4
+ import { RefusalError } from '../environment/contract.js';
5
+ import { recordNumber, withStatus } from '../docs/decision-record.js';
6
+ import { splitRepositoryPath } from '../repository-path.js';
7
+ import { citedOrders, holdsPlaceholders, namesScope, nextRecordName, recordTitle, scaffoldRecord } from './decisions.js';
8
+ import { complete, now, refusal, TERMINAL } from './application-context.js';
9
+ // Decisions recorded on an order, each with its latest resolution.
10
+ export const DECISIONS_UNDECLARED = 'decision records need a directory: declare one in cockpit.toml, for example [docs] decisions = "docs/adr", '
11
+ + 'with decisions_index = "docs/adr/README.md" if the records have an index';
12
+ export function decisions(record) {
13
+ return record.notes.flatMap((row, index) => row.kind !== 'decision' ? [] : [{ decision: index + 1, title: recordTitle(row.text),
14
+ resolution: record.notes.findLast(note => note.kind === 'resolution' && note.resolves === index + 1) }]);
15
+ }
16
+ // Why a cited record no longer stands for its decision; `filled` also requires that the template's placeholders are gone.
17
+ export function recordProblem(text, reference, orderId, filled) {
18
+ if (text === undefined)
19
+ return `its record ${reference} does not exist`;
20
+ if (!citedOrders(text).includes(orderId))
21
+ return `its record ${reference} does not cite ${orderId}; add the line "Work order: ${orderId}"`;
22
+ return filled && holdsPlaceholders(text) ? `its record ${reference} still holds the template's placeholders; replace each "<!-- cockpit: replace this ... -->" comment` : undefined;
23
+ }
24
+ export async function unresolvedDecisions(record, composition) {
25
+ const rows = [];
26
+ for (const row of decisions(record)) {
27
+ const name = `decision ${row.decision} (${row.title})`;
28
+ if (!row.resolution)
29
+ rows.push(`${name} is unresolved; run npx --no cockpit work decide --config <your cockpit.toml> --work-order ${record.id} --decision ${row.decision} --record, or with --none --reason "<why>"`);
30
+ else if (row.resolution.record) {
31
+ const problem = recordProblem(await composition.readDocsFile(row.resolution.record), row.resolution.record, record.id, true);
32
+ if (problem)
33
+ rows.push(`${name}: ${problem}`);
34
+ }
35
+ }
36
+ return rows;
37
+ }
38
+ // Decision records whose text names a path the order may change, so earlier decisions about that code are in front of the agent.
39
+ export async function relatedDecisions(context, order) {
40
+ const { composition } = context;
41
+ const directory = composition.docs?.decisions;
42
+ if (!directory)
43
+ return {};
44
+ const { slug } = splitRepositoryPath(directory);
45
+ const prefix = slug === undefined ? '' : `${slug}:`;
46
+ let read;
47
+ // The listing is advice, so a directory cockpit cannot read is reported here and refused where a record is written.
48
+ try {
49
+ read = await context.decisionRecords();
50
+ }
51
+ catch (error) {
52
+ if (!(error instanceof RefusalError))
53
+ throw error;
54
+ return { relatedDecisionsUnavailable: error.message };
55
+ }
56
+ return { relatedDecisions: [...read.entries()]
57
+ .filter(([reference, text]) => reference !== composition.docs?.decisionsIndex && namesScope(text, order.scope, prefix)).map(([reference]) => reference) };
58
+ }
59
+ export async function decideOrder(request, context) {
60
+ const { composition, operation, store } = context;
61
+ const directory = composition.docs?.decisions;
62
+ if (!directory)
63
+ return refusal(operation, DECISIONS_UNDECLARED);
64
+ const orderId = request.workOrderId;
65
+ const record = await store.read(orderId);
66
+ if (!record || TERMINAL.has(record.state))
67
+ return refusal(operation, `${orderId} has no open record to resolve a decision on`);
68
+ const decision = record.notes[request.decision - 1];
69
+ if (decision?.kind !== 'decision')
70
+ return refusal(operation, `note ${request.decision} on ${orderId} is not a decision; work status lists each order's open decisions`);
71
+ const resolve = (fields, writes) => store.update(orderId, async (existing) => {
72
+ if (!existing || TERMINAL.has(existing.state))
73
+ throw new RefusalError(`${orderId} closed while its decision was being resolved`);
74
+ await writes?.();
75
+ return { record: { ...existing, notes: [...existing.notes, { at: now(), by: 'agent', kind: 'resolution', resolves: request.decision, ...fields }] }, value: undefined };
76
+ });
77
+ const resolution = request.resolution;
78
+ if (resolution.kind === 'none') {
79
+ await resolve({ text: resolution.reason });
80
+ return complete(operation, { id: orderId, decision: request.decision, resolution: 'none', reason: resolution.reason });
81
+ }
82
+ if (resolution.path !== undefined) {
83
+ if (!resolution.path.startsWith(`${directory}/`))
84
+ return refusal(operation, `${resolution.path} is not inside [docs] decisions ${directory}; write the record there`);
85
+ if (resolution.supersedes)
86
+ return refusal(operation, '--supersedes applies when cockpit writes the record; with a path, write the Supersedes line and the old record\'s status yourself');
87
+ const problem = recordProblem(await composition.readDocsFile(resolution.path), resolution.path, orderId, false);
88
+ if (problem)
89
+ return refusal(operation, `${resolution.path} cannot resolve decision ${request.decision}: ${problem}`);
90
+ await resolve({ text: `recorded in ${resolution.path}`, record: resolution.path });
91
+ return complete(operation, { id: orderId, decision: request.decision, record: resolution.path });
92
+ }
93
+ // Scaffold the record; the agent fills in its body, and signoff refuses while the template's placeholders remain.
94
+ const existing = await context.decisionRecords();
95
+ const names = [...existing.keys()].map(reference => reference.slice(directory.length + 1));
96
+ const title = recordTitle(decision.text);
97
+ const named = nextRecordName(names, title, now().slice(0, 10));
98
+ const reference = `${directory}/${named.name}`;
99
+ const writes = [{ reference, create: true, text: '' }];
100
+ let supersedes;
101
+ if (resolution.supersedes) {
102
+ if (!named.number)
103
+ return refusal(operation, `the records in ${directory} are named by date, so none has a number to supersede`);
104
+ const old = names.filter(name => Number(recordNumber(name)) === Number(resolution.supersedes));
105
+ if (old.length !== 1)
106
+ return refusal(operation, `${directory} has ${old.length ? `${old.length} records` : 'no record'} numbered ${resolution.supersedes}; pass the number of exactly one`);
107
+ supersedes = recordNumber(old[0]);
108
+ const marked = withStatus(existing.get(`${directory}/${old[0]}`), `superseded by ${named.number}`);
109
+ if (marked === undefined)
110
+ return refusal(operation, `${directory}/${old[0]} has no Status line to mark superseded; add "Status: accepted" to it`);
111
+ writes.push({ reference: `${directory}/${old[0]}`, create: false, text: marked });
112
+ }
113
+ writes[0].text = scaffoldRecord({ title, workOrder: orderId, ...named.number ? { number: named.number } : {}, ...supersedes ? { supersedes } : {} });
114
+ const index = composition.docs?.decisionsIndex;
115
+ if (index !== undefined && await composition.readDocsFile(index) === undefined)
116
+ return refusal(operation, `[docs] decisions_index ${index} does not exist; create it, or remove decisions_index from cockpit.toml`);
117
+ const link = index === undefined ? undefined : posix.relative(posix.dirname(splitRepositoryPath(index).path), splitRepositoryPath(reference).path);
118
+ await resolve({ text: `recorded in ${reference}`, record: reference }, () => composition.writeDecisionRecords(writes, link === undefined ? undefined : `- [${named.number ? `${named.number}. ` : ''}${title}](${link})`));
119
+ return complete(operation, { id: orderId, decision: request.decision, record: reference, written: writes.map(row => row.reference),
120
+ next: `fill in ${reference}: signoff refuses while its "cockpit: replace this" placeholders remain` });
121
+ }
@@ -0,0 +1,11 @@
1
+ import type { WorkApplicationComposition } from './composition.js';
2
+ import { type WorkApplicationOutcome, type WorkApplicationRequest, type WorkContext, type WorkRequest } from './application-context.js';
3
+ export declare function testRun(captured: Extract<WorkApplicationRequest, {
4
+ operation: 'test-run';
5
+ }>, composition: WorkApplicationComposition): Promise<WorkApplicationOutcome>;
6
+ export declare function gradeOrder(request: Extract<WorkRequest, {
7
+ operation: 'work-grade';
8
+ }>, context: WorkContext): Promise<WorkApplicationOutcome>;
9
+ export declare function gradeCriteriaFile(request: Extract<WorkRequest, {
10
+ operation: 'grade-criteria';
11
+ }>, context: WorkContext): Promise<WorkApplicationOutcome>;