@etiquekit/etq 1.0.12 → 1.0.14

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 (149) hide show
  1. package/AGENTS.md +19 -26
  2. package/LICENSE +2 -3
  3. package/NOTICE +6 -4
  4. package/QuickStart.md +35 -47
  5. package/README.md +54 -54
  6. package/bin/etiquette +1 -5
  7. package/bin/etiquette-core +1 -5
  8. package/docs/ARCHITECTURE.md +5 -5
  9. package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
  10. package/docs/CONCEPTS.md +7 -23
  11. package/docs/CONCEPT_STATUS.md +66 -0
  12. package/docs/CORE_PROFILE.md +3 -5
  13. package/docs/LANE_PROVISIONING.md +7 -2
  14. package/docs/README.md +4 -4
  15. package/docs/RELEASE_SURFACE_AUDIT.md +8 -7
  16. package/docs/SEAT_DISCIPLINE.md +55 -91
  17. package/docs/SEAT_PROVISIONING.md +19 -22
  18. package/docs/TEAM_HANDOFF.md +21 -21
  19. package/docs/WORKTREE_QOL.md +7 -6
  20. package/docs/contracts/ledger-entry/README.md +11 -11
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
  22. package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
  23. package/package.json +9 -9
  24. package/packages/control/src/authority/lease.ts +261 -0
  25. package/packages/control/src/authority/node-delegation.ts +257 -0
  26. package/packages/control/src/authority/rig-conductor.ts +632 -0
  27. package/packages/control/src/cli/argv.ts +155 -0
  28. package/packages/control/src/cli/commands/console.ts +200 -0
  29. package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
  30. package/packages/control/src/cli/commands/dispatch.ts +279 -0
  31. package/packages/control/src/cli/commands/harness.ts +89 -0
  32. package/packages/control/src/cli/commands/hook.ts +50 -0
  33. package/packages/control/src/cli/commands/ledger.ts +91 -0
  34. package/packages/control/src/cli/commands/local-workflow.ts +4690 -0
  35. package/packages/control/src/cli/commands/memory.ts +445 -0
  36. package/packages/control/src/cli/commands/release.ts +108 -0
  37. package/packages/control/src/cli/commands/rubric.ts +103 -0
  38. package/packages/control/src/cli/commands/seat.ts +179 -0
  39. package/packages/control/src/cli/commands/session.ts +127 -0
  40. package/packages/control/src/cli/commands/supervision.ts +246 -0
  41. package/packages/control/src/cli/commands/sync.ts +119 -0
  42. package/packages/control/src/cli/commands/workflow.ts +86 -0
  43. package/packages/control/src/cli/commands/workspace.ts +50 -0
  44. package/packages/control/src/cli/core-usage.ts +34 -0
  45. package/packages/control/src/cli/prompt.ts +67 -0
  46. package/packages/control/src/cli/supervision-deps.ts +44 -0
  47. package/packages/control/src/cli/usage.ts +241 -0
  48. package/packages/control/src/cli.ts +207 -0
  49. package/packages/control/src/core-cli.ts +50 -0
  50. package/packages/control/src/dispatch/decision.ts +202 -0
  51. package/packages/control/src/dispatch/projection.ts +293 -0
  52. package/packages/control/src/dispatch/record.ts +153 -0
  53. package/packages/control/src/engagement/project.ts +170 -0
  54. package/packages/control/src/fs.ts +19 -0
  55. package/packages/control/src/harness/pruning.ts +406 -0
  56. package/packages/control/src/hooks/dispatcher.ts +117 -0
  57. package/packages/control/src/hooks/outbox.ts +86 -0
  58. package/packages/control/src/hooks/sanitize.ts +6 -0
  59. package/packages/control/src/hooks/types.ts +34 -0
  60. package/packages/control/src/index.ts +384 -0
  61. package/packages/control/src/ledger/entry.ts +303 -0
  62. package/packages/control/src/ledger/indexer.ts +542 -0
  63. package/packages/control/src/memory/context.ts +149 -0
  64. package/packages/control/src/memory/drain-import.ts +207 -0
  65. package/packages/control/src/memory/indexer.ts +284 -0
  66. package/packages/control/src/memory/query.ts +75 -0
  67. package/packages/control/src/memory/sanitize.ts +50 -0
  68. package/packages/control/src/memory/sharded-drain-import.ts +212 -0
  69. package/packages/control/src/memory/status.ts +211 -0
  70. package/packages/control/src/memory/store-lifecycle.ts +509 -0
  71. package/packages/control/src/memory/store.ts +284 -0
  72. package/packages/control/src/memory/types.ts +146 -0
  73. package/packages/control/src/parity/surfaces.ts +748 -0
  74. package/packages/control/src/project.ts +141 -0
  75. package/packages/control/src/projection/local-ledger-view.ts +373 -0
  76. package/packages/control/src/projection/return-enforcement.ts +48 -0
  77. package/packages/control/src/projection/timeline-preview.ts +539 -0
  78. package/packages/control/src/projection/timeline.ts +708 -0
  79. package/packages/control/src/release/readiness.ts +842 -0
  80. package/packages/control/src/rubric/loader.ts +326 -0
  81. package/packages/control/src/rubric/promotion.ts +54 -0
  82. package/packages/control/src/rubric/runner.ts +159 -0
  83. package/packages/control/src/rubric/types.ts +158 -0
  84. package/packages/control/src/seat/owner-card.ts +388 -0
  85. package/packages/control/src/seat/readiness.ts +834 -0
  86. package/packages/control/src/session/runbook.ts +431 -0
  87. package/packages/control/src/shared/sanitize.ts +49 -0
  88. package/packages/control/src/supervision/action-classes.ts +192 -0
  89. package/packages/control/src/supervision/command-apply.ts +378 -0
  90. package/packages/control/src/supervision/errors.ts +14 -0
  91. package/packages/control/src/supervision/event-replay.ts +155 -0
  92. package/packages/control/src/supervision/events.ts +109 -0
  93. package/packages/control/src/supervision/index.ts +16 -0
  94. package/packages/control/src/supervision/manifest.ts +127 -0
  95. package/packages/control/src/supervision/paths.ts +49 -0
  96. package/packages/control/src/supervision/projection-adapter.ts +274 -0
  97. package/packages/control/src/supervision/projection.ts +75 -0
  98. package/packages/control/src/supervision/rebuild.ts +99 -0
  99. package/packages/control/src/supervision/session-open.ts +131 -0
  100. package/packages/control/src/supervision/session-read.ts +99 -0
  101. package/packages/control/src/supervision/sqlite-impl.ts +71 -0
  102. package/packages/control/src/supervision/sqlite.ts +121 -0
  103. package/packages/control/src/supervision/store-rows.ts +371 -0
  104. package/packages/control/src/supervision/turn-close.ts +154 -0
  105. package/packages/control/src/supervision/turn-open.ts +284 -0
  106. package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
  107. package/packages/control/src/sync/join-plan.ts +375 -0
  108. package/packages/control/src/sync/outbox.ts +492 -0
  109. package/packages/control/src/workflow/evaluator.ts +140 -0
  110. package/packages/control/src/workflow/loader.ts +200 -0
  111. package/packages/control/src/workflow/types.ts +90 -0
  112. package/packages/control/src/workspace/authority.ts +499 -0
  113. package/packages/protocol/src/guards.ts +119 -0
  114. package/packages/protocol/src/huddle-board.ts +198 -0
  115. package/packages/protocol/src/huddle.ts +295 -0
  116. package/packages/protocol/src/incident.ts +251 -0
  117. package/packages/protocol/src/index.ts +8 -0
  118. package/packages/protocol/src/interfaces.ts +107 -0
  119. package/packages/protocol/src/packet-profile.ts +195 -0
  120. package/packages/protocol/src/state.ts +81 -0
  121. package/packages/protocol/src/types.ts +434 -0
  122. package/release/lineage.v0.json +15 -0
  123. package/scripts/release-candidate-verify.sh +175 -0
  124. package/scripts/release-checksum.sh +25 -0
  125. package/scripts/release-pack-canary.sh +97 -0
  126. package/scripts/release-scan.sh +249 -0
  127. package/scripts/release-sign.sh +34 -0
  128. package/templates/etiquette-vanilla-v0/README.md +2 -3
  129. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
  130. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
  131. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
  132. package/templates/seat-packs-v0/README.md +3 -2
  133. package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
  134. package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
  135. package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
  136. package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
  137. package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
  138. package/docs/AGENT_SPAWNING.md +0 -78
  139. package/docs/INTEGRATION_UI.md +0 -215
  140. package/docs/OPERATOR_PLAYBOOK.md +0 -77
  141. package/docs/TAG_ROUTE.md +0 -95
  142. package/lib/etiquette-core.js +0 -315
  143. package/lib/etiquette.js +0 -1160
  144. package/templates/DISPATCH_PREMISES.md +0 -39
  145. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
  146. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
  147. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
  148. package/templates/hosted-receiver/README.md +0 -41
  149. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
@@ -0,0 +1,198 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ HUDDLE_CARD_MODES,
4
+ HuddleCardV1Schema,
5
+ HuddleExitReceiptV1Schema,
6
+ type HuddleCardV1,
7
+ type HuddleExitReceiptV1,
8
+ } from './huddle';
9
+
10
+ export const HUDDLE_BOARD_STATUSES = [
11
+ 'ready',
12
+ 'claimed',
13
+ 'in_progress',
14
+ 'waiting_return',
15
+ 'accepted',
16
+ 'blocked',
17
+ 'stale',
18
+ ] as const;
19
+
20
+ export type HuddleBoardStatus = (typeof HUDDLE_BOARD_STATUSES)[number];
21
+
22
+ const nonEmptyString = z.string().trim().min(1);
23
+ export const HUDDLE_BOARD_GENERATED_NOTICE =
24
+ 'This is a read replica. Do not hand-edit; update source cards or receipts and regenerate.' as const;
25
+
26
+ export const HuddleBoardItemV1Schema = z.object({
27
+ key: nonEmptyString,
28
+ repo: nonEmptyString,
29
+ lane: nonEmptyString,
30
+ task_id: nonEmptyString,
31
+ mode: z.enum(HUDDLE_CARD_MODES),
32
+ status: z.enum(HUDDLE_BOARD_STATUSES),
33
+ card_status: z.enum(HUDDLE_BOARD_STATUSES),
34
+ current_owner: nonEmptyString,
35
+ next_owner: nonEmptyString,
36
+ card_ref: nonEmptyString.optional(),
37
+ receipt_ref: nonEmptyString.optional(),
38
+ receipt_verdict: z.enum(['accept', 'counter', 'blocked']).optional(),
39
+ stale_reason: nonEmptyString.optional(),
40
+ evidence_refs: z.array(nonEmptyString).default([]),
41
+ });
42
+
43
+ export const HuddleBoardV1Schema = z.object({
44
+ schema_version: z.literal('huddle-board.v1'),
45
+ generated_notice: z.literal(HUDDLE_BOARD_GENERATED_NOTICE),
46
+ generated_at: nonEmptyString,
47
+ summary: z.record(z.enum(HUDDLE_BOARD_STATUSES), z.number().int().min(0)),
48
+ items: z.array(HuddleBoardItemV1Schema),
49
+ });
50
+
51
+ export type HuddleBoardItemV1 = z.infer<typeof HuddleBoardItemV1Schema>;
52
+ export type HuddleBoardV1 = z.infer<typeof HuddleBoardV1Schema>;
53
+
54
+ export type HuddleCardRef = {
55
+ card: HuddleCardV1;
56
+ ref?: string;
57
+ };
58
+
59
+ export type HuddleReceiptRef = {
60
+ receipt: HuddleExitReceiptV1;
61
+ ref?: string;
62
+ };
63
+
64
+ function taskKey(repo: string, lane: string, taskId: string): string {
65
+ return `${repo}::${lane}::${taskId}`;
66
+ }
67
+
68
+ function isExpired(value: string, now: Date): boolean {
69
+ const parsed = Date.parse(value);
70
+ return Number.isFinite(parsed) && parsed < now.getTime();
71
+ }
72
+
73
+ function emptySummary(): Record<HuddleBoardStatus, number> {
74
+ return {
75
+ ready: 0,
76
+ claimed: 0,
77
+ in_progress: 0,
78
+ waiting_return: 0,
79
+ accepted: 0,
80
+ blocked: 0,
81
+ stale: 0,
82
+ };
83
+ }
84
+
85
+ export function buildHuddleBoardV1(input: {
86
+ cards: HuddleCardRef[];
87
+ receipts?: HuddleReceiptRef[];
88
+ generated_at?: string;
89
+ now?: Date;
90
+ }): HuddleBoardV1 {
91
+ const now = input.now ?? new Date(input.generated_at ?? Date.now());
92
+ const generatedAt = input.generated_at ?? now.toISOString();
93
+ const receiptsByKey = new Map<string, HuddleReceiptRef>();
94
+
95
+ for (const candidate of input.receipts ?? []) {
96
+ const receipt = HuddleExitReceiptV1Schema.parse(candidate.receipt);
97
+ receiptsByKey.set(taskKey(receipt.repo, receipt.lane, receipt.task_id), {
98
+ receipt,
99
+ ref: candidate.ref,
100
+ });
101
+ }
102
+
103
+ const items: HuddleBoardItemV1[] = input.cards.map((candidate) => {
104
+ const card = HuddleCardV1Schema.parse(candidate.card);
105
+ const key = taskKey(card.repo, card.lane, card.task_id);
106
+ const receiptRef = receiptsByKey.get(key);
107
+ const expired = isExpired(card.expires_or_refresh_after, now);
108
+
109
+ let status: HuddleBoardStatus = card.status;
110
+ let staleReason: string | undefined;
111
+ const evidenceRefs: string[] = [];
112
+
113
+ if (receiptRef) {
114
+ status = receiptRef.receipt.verdict === 'accept' ? 'accepted' : 'blocked';
115
+ evidenceRefs.push(receiptRef.receipt.source_ref, ...receiptRef.receipt.evidence);
116
+ } else if (expired && card.status !== 'accepted' && card.status !== 'blocked') {
117
+ status = 'stale';
118
+ staleReason = `expired after ${card.expires_or_refresh_after}`;
119
+ }
120
+
121
+ return HuddleBoardItemV1Schema.parse({
122
+ key,
123
+ repo: card.repo,
124
+ lane: card.lane,
125
+ task_id: card.task_id,
126
+ mode: card.mode,
127
+ status,
128
+ card_status: card.status,
129
+ current_owner: card.current_owner,
130
+ next_owner: receiptRef?.receipt.next_owner ?? card.next_owner,
131
+ card_ref: candidate.ref,
132
+ receipt_ref: receiptRef?.ref,
133
+ receipt_verdict: receiptRef?.receipt.verdict,
134
+ stale_reason: staleReason,
135
+ evidence_refs: evidenceRefs,
136
+ });
137
+ }).sort((left, right) => left.key.localeCompare(right.key));
138
+
139
+ const summary = emptySummary();
140
+ for (const item of items) {
141
+ summary[item.status] += 1;
142
+ }
143
+
144
+ return HuddleBoardV1Schema.parse({
145
+ schema_version: 'huddle-board.v1',
146
+ generated_notice: HUDDLE_BOARD_GENERATED_NOTICE,
147
+ generated_at: generatedAt,
148
+ summary,
149
+ items,
150
+ });
151
+ }
152
+
153
+ export function stableHuddleBoardFingerprint(board: HuddleBoardV1): string {
154
+ return JSON.stringify({
155
+ schema_version: board.schema_version,
156
+ generated_notice: board.generated_notice,
157
+ summary: board.summary,
158
+ items: board.items.map((item) => ({
159
+ key: item.key,
160
+ status: item.status,
161
+ current_owner: item.current_owner,
162
+ next_owner: item.next_owner,
163
+ receipt_verdict: item.receipt_verdict,
164
+ stale_reason: item.stale_reason,
165
+ })),
166
+ });
167
+ }
168
+
169
+ export function renderHuddleBoardMarkdown(board: HuddleBoardV1): string {
170
+ const lines = [
171
+ '# Huddle Board',
172
+ '',
173
+ `> ${board.generated_notice}`,
174
+ '',
175
+ `Generated: ${board.generated_at}`,
176
+ '',
177
+ '| Status | Count |',
178
+ '| --- | ---: |',
179
+ ...HUDDLE_BOARD_STATUSES.map((status) => `| ${status} | ${board.summary[status]} |`),
180
+ '',
181
+ '| Task | Repo | Lane | Status | Owner | Next | Receipt |',
182
+ '| --- | --- | --- | --- | --- | --- | --- |',
183
+ ];
184
+
185
+ for (const item of board.items) {
186
+ lines.push([
187
+ item.task_id,
188
+ item.repo,
189
+ item.lane,
190
+ item.status,
191
+ item.current_owner,
192
+ item.next_owner,
193
+ item.receipt_ref ?? '',
194
+ ].map((value) => String(value).replaceAll('|', '\\|')).join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
195
+ }
196
+
197
+ return `${lines.join('\n')}\n`;
198
+ }
@@ -0,0 +1,295 @@
1
+ import { z } from 'zod';
2
+
3
+ export const HUDDLE_CARD_MODES = ['read-only', 'docs-only', 'implementation', 'review'] as const;
4
+ export const HUDDLE_CARD_STATUSES = [
5
+ 'ready',
6
+ 'claimed',
7
+ 'in_progress',
8
+ 'waiting_return',
9
+ 'accepted',
10
+ 'blocked',
11
+ 'stale',
12
+ ] as const;
13
+ export const HUDDLE_EXIT_RECEIPT_VERDICTS = ['accept', 'counter', 'blocked'] as const;
14
+ export const PORTABLE_EXIT_RECEIPT_OUTCOMES = ['success', 'partial', 'blocked'] as const;
15
+
16
+ export type HuddleCardMode = (typeof HUDDLE_CARD_MODES)[number];
17
+ export type HuddleCardStatus = (typeof HUDDLE_CARD_STATUSES)[number];
18
+ export type HuddleExitReceiptVerdict = (typeof HUDDLE_EXIT_RECEIPT_VERDICTS)[number];
19
+ export type PortableExitReceiptOutcome = (typeof PORTABLE_EXIT_RECEIPT_OUTCOMES)[number];
20
+
21
+ const nonEmptyString = z.string().trim().min(1);
22
+
23
+ const stringList = z.array(nonEmptyString);
24
+ const validationEntry = z.union([
25
+ nonEmptyString,
26
+ z.object({
27
+ command: nonEmptyString,
28
+ result: nonEmptyString,
29
+ }).passthrough(),
30
+ ]);
31
+
32
+ function looksLikeChatMemoryPath(value: string): boolean {
33
+ const normalized = value.toLowerCase();
34
+ return (
35
+ normalized.includes('chat-history') ||
36
+ normalized.includes('chat_history') ||
37
+ normalized.includes('chatlog') ||
38
+ normalized.includes('chat-memory') ||
39
+ normalized.includes('conversation-log') ||
40
+ normalized.includes('transcript')
41
+ );
42
+ }
43
+
44
+ export const HuddleCardV1Schema = z.object({
45
+ schema_version: z.literal('huddle-card.v1'),
46
+ repo: nonEmptyString,
47
+ repo_path: nonEmptyString.optional(),
48
+ lane: nonEmptyString,
49
+ task_id: nonEmptyString,
50
+ mode: z.enum(HUDDLE_CARD_MODES),
51
+ status: z.enum(HUDDLE_CARD_STATUSES).default('ready'),
52
+ startup_docs: stringList.min(1),
53
+ task_packet: nonEmptyString.optional(),
54
+ allowed_writes: stringList,
55
+ forbidden_sources: stringList,
56
+ validation: stringList.min(1),
57
+ stop_conditions: stringList.min(1),
58
+ expected_return: stringList.min(1),
59
+ evidence_sink: nonEmptyString.optional(),
60
+ current_owner: nonEmptyString,
61
+ next_owner: nonEmptyString,
62
+ expires_or_refresh_after: nonEmptyString,
63
+ }).superRefine((card, ctx) => {
64
+ for (const [index, source] of card.startup_docs.entries()) {
65
+ if (looksLikeChatMemoryPath(source)) {
66
+ ctx.addIssue({
67
+ code: 'custom',
68
+ path: ['startup_docs', index],
69
+ message: 'startup_docs must point to repo-local source of truth, not chat logs or transcripts',
70
+ });
71
+ }
72
+ }
73
+
74
+ const forbiddenText = card.forbidden_sources.join(' ').toLowerCase();
75
+ if (!forbiddenText.includes('chat') && !forbiddenText.includes('transcript')) {
76
+ ctx.addIssue({
77
+ code: 'custom',
78
+ path: ['forbidden_sources'],
79
+ message: 'forbidden_sources should explicitly ban chat/transcript memory for rotation-safe pickup',
80
+ });
81
+ }
82
+
83
+ if (card.mode === 'implementation') {
84
+ const usefulWrites = card.allowed_writes.filter((entry) => entry !== 'read-only');
85
+ if (usefulWrites.length === 0) {
86
+ ctx.addIssue({
87
+ code: 'custom',
88
+ path: ['allowed_writes'],
89
+ message: 'implementation cards must declare concrete allowed_writes',
90
+ });
91
+ }
92
+ }
93
+ });
94
+
95
+ export type HuddleCardV1 = z.infer<typeof HuddleCardV1Schema>;
96
+
97
+ export const HuddleExitReceiptV1Schema = z.object({
98
+ schema_version: z.literal('huddle-exit-receipt.v1'),
99
+ task_id: nonEmptyString,
100
+ verdict: z.enum(HUDDLE_EXIT_RECEIPT_VERDICTS),
101
+ repo: nonEmptyString,
102
+ lane: nonEmptyString,
103
+ source_ref: nonEmptyString,
104
+ changed_files: z.array(nonEmptyString).default([]),
105
+ validation: z.array(nonEmptyString).default([]),
106
+ evidence: stringList.min(1),
107
+ blockers: z.array(nonEmptyString).default([]),
108
+ next_owner: nonEmptyString,
109
+ refresh_after: nonEmptyString,
110
+ }).superRefine((receipt, ctx) => {
111
+ if (looksLikeChatMemoryPath(receipt.source_ref)) {
112
+ ctx.addIssue({
113
+ code: 'custom',
114
+ path: ['source_ref'],
115
+ message: 'source_ref must point to a durable task packet, commit, issue, bus event, or repo-local receipt; not chat logs or transcripts',
116
+ });
117
+ }
118
+
119
+ for (const [index, evidence] of receipt.evidence.entries()) {
120
+ if (looksLikeChatMemoryPath(evidence)) {
121
+ ctx.addIssue({
122
+ code: 'custom',
123
+ path: ['evidence', index],
124
+ message: 'evidence must be repo-local or packet-local durable proof, not chat logs or transcripts',
125
+ });
126
+ }
127
+ }
128
+
129
+ if (receipt.verdict !== 'accept' && receipt.blockers.length === 0) {
130
+ ctx.addIssue({
131
+ code: 'custom',
132
+ path: ['blockers'],
133
+ message: 'counter and blocked receipts must name at least one blocker',
134
+ });
135
+ }
136
+ });
137
+
138
+ export type HuddleExitReceiptV1 = z.infer<typeof HuddleExitReceiptV1Schema>;
139
+
140
+ export const PortableExitReceiptV1Schema = z.object({
141
+ schema_version: z.literal('exit-receipt.v1'),
142
+ receipt_id: nonEmptyString,
143
+ work_id: nonEmptyString,
144
+ outcome: z.enum(PORTABLE_EXIT_RECEIPT_OUTCOMES),
145
+ changed_files: z.array(nonEmptyString).default([]),
146
+ validation: z.array(validationEntry).default([]),
147
+ evidence_refs: stringList.min(1),
148
+ stop_conditions: stringList.min(1),
149
+ deferred_work: z.array(nonEmptyString).default([]),
150
+ next_owner: nonEmptyString,
151
+ }).superRefine((receipt, ctx) => {
152
+ for (const [index, evidence] of receipt.evidence_refs.entries()) {
153
+ if (looksLikeChatMemoryPath(evidence)) {
154
+ ctx.addIssue({
155
+ code: 'custom',
156
+ path: ['evidence_refs', index],
157
+ message: 'evidence_refs must be durable proof, not chat logs or transcripts',
158
+ });
159
+ }
160
+ }
161
+ });
162
+
163
+ export type PortableExitReceiptV1 = z.infer<typeof PortableExitReceiptV1Schema>;
164
+
165
+ export function parseHuddleCardV1(value: unknown): HuddleCardV1 {
166
+ return HuddleCardV1Schema.parse(value);
167
+ }
168
+
169
+ export function validateHuddleCardV1(value: unknown): void {
170
+ HuddleCardV1Schema.parse(value);
171
+ }
172
+
173
+ export function parseHuddleExitReceiptV1(value: unknown): HuddleExitReceiptV1 {
174
+ return HuddleExitReceiptV1Schema.parse(value);
175
+ }
176
+
177
+ export function validateHuddleExitReceiptV1(value: unknown): void {
178
+ HuddleExitReceiptV1Schema.parse(value);
179
+ }
180
+
181
+ export function parsePortableExitReceiptV1(value: unknown): PortableExitReceiptV1 {
182
+ return PortableExitReceiptV1Schema.parse(value);
183
+ }
184
+
185
+ export function validatePortableExitReceiptV1(value: unknown): void {
186
+ PortableExitReceiptV1Schema.parse(value);
187
+ }
188
+
189
+ function huddleVerdictToPortableOutcome(verdict: HuddleExitReceiptVerdict): PortableExitReceiptOutcome {
190
+ if (verdict === 'accept') {
191
+ return 'success';
192
+ }
193
+ if (verdict === 'counter') {
194
+ return 'partial';
195
+ }
196
+ return 'blocked';
197
+ }
198
+
199
+ function portableOutcomeToHuddleVerdict(outcome: PortableExitReceiptOutcome): HuddleExitReceiptVerdict {
200
+ if (outcome === 'success') {
201
+ return 'accept';
202
+ }
203
+ if (outcome === 'partial') {
204
+ return 'counter';
205
+ }
206
+ return 'blocked';
207
+ }
208
+
209
+ function renderValidationEntry(entry: PortableExitReceiptV1['validation'][number]): string {
210
+ if (typeof entry === 'string') {
211
+ return entry;
212
+ }
213
+ return `${entry.command} -> ${entry.result}`;
214
+ }
215
+
216
+ export function toPortableExitReceiptV1(
217
+ receipt: HuddleExitReceiptV1,
218
+ receiptId = `${receipt.task_id}-exit-receipt`,
219
+ ): PortableExitReceiptV1 {
220
+ return PortableExitReceiptV1Schema.parse({
221
+ schema_version: 'exit-receipt.v1',
222
+ receipt_id: receiptId,
223
+ work_id: receipt.task_id,
224
+ outcome: huddleVerdictToPortableOutcome(receipt.verdict),
225
+ changed_files: receipt.changed_files,
226
+ validation: receipt.validation,
227
+ evidence_refs: [receipt.source_ref, ...receipt.evidence],
228
+ stop_conditions: receipt.blockers.length > 0 ? receipt.blockers : ['none fired'],
229
+ deferred_work: [],
230
+ next_owner: receipt.next_owner,
231
+ });
232
+ }
233
+
234
+ export function toHuddleExitReceiptV1(
235
+ receipt: PortableExitReceiptV1,
236
+ context: {
237
+ repo: string;
238
+ lane: string;
239
+ source_ref?: string;
240
+ refresh_after?: string;
241
+ },
242
+ ): HuddleExitReceiptV1 {
243
+ const blockers = receipt.outcome === 'success'
244
+ ? []
245
+ : [...receipt.stop_conditions, ...receipt.deferred_work].filter((entry) => entry !== 'none fired');
246
+
247
+ return HuddleExitReceiptV1Schema.parse({
248
+ schema_version: 'huddle-exit-receipt.v1',
249
+ task_id: receipt.work_id,
250
+ verdict: portableOutcomeToHuddleVerdict(receipt.outcome),
251
+ repo: context.repo,
252
+ lane: context.lane,
253
+ source_ref: context.source_ref ?? receipt.evidence_refs[0],
254
+ changed_files: receipt.changed_files,
255
+ validation: receipt.validation.map(renderValidationEntry),
256
+ evidence: receipt.evidence_refs,
257
+ blockers,
258
+ next_owner: receipt.next_owner,
259
+ refresh_after: context.refresh_after ?? 'after repo-local state changes',
260
+ });
261
+ }
262
+
263
+ export function summarizeHuddleCard(card: HuddleCardV1): string {
264
+ return [
265
+ `${card.task_id} [${card.status}/${card.mode}]`,
266
+ `repo=${card.repo}`,
267
+ `lane=${card.lane}`,
268
+ `owner=${card.current_owner}`,
269
+ `next=${card.next_owner}`,
270
+ `startup_docs=${card.startup_docs.length}`,
271
+ `validation=${card.validation.length}`,
272
+ ].join(' ');
273
+ }
274
+
275
+ export function summarizeHuddleExitReceipt(receipt: HuddleExitReceiptV1): string {
276
+ return [
277
+ `${receipt.task_id} [${receipt.verdict}]`,
278
+ `repo=${receipt.repo}`,
279
+ `lane=${receipt.lane}`,
280
+ `source=${receipt.source_ref}`,
281
+ `changed_files=${receipt.changed_files.length}`,
282
+ `validation=${receipt.validation.length}`,
283
+ `next=${receipt.next_owner}`,
284
+ ].join(' ');
285
+ }
286
+
287
+ export function summarizePortableExitReceipt(receipt: PortableExitReceiptV1): string {
288
+ return [
289
+ `${receipt.work_id} [${receipt.outcome}]`,
290
+ `receipt=${receipt.receipt_id}`,
291
+ `changed_files=${receipt.changed_files.length}`,
292
+ `validation=${receipt.validation.length}`,
293
+ `next=${receipt.next_owner}`,
294
+ ].join(' ');
295
+ }