@astrosheep/square 0.3.70 → 0.3.71

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 (115) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/perception-projection.d.ts +6 -15
  51. package/dist/perception-projection.js +6 -26
  52. package/dist/plugins/opencode/delivery.js +9 -9
  53. package/dist/plugins/opencode/index.js +3 -3
  54. package/dist/ports.d.ts +156 -11
  55. package/dist/presentation-operations.d.ts +1 -1
  56. package/dist/presentation-operations.js +35 -44
  57. package/dist/presentation.d.ts +12 -19
  58. package/dist/presentation.js +33 -54
  59. package/dist/routes.d.ts +7 -14
  60. package/dist/routes.js +33 -62
  61. package/dist/runtime.d.ts +12 -39
  62. package/dist/runtime.js +33 -164
  63. package/dist/square-actions.d.ts +11 -21
  64. package/dist/square-actions.js +171 -271
  65. package/dist/square-application.d.ts +31 -55
  66. package/dist/square-application.js +637 -141
  67. package/dist/square-core.d.ts +3 -94
  68. package/dist/square-core.js +8 -353
  69. package/dist/square-domain.d.ts +24 -0
  70. package/dist/square-domain.js +77 -0
  71. package/dist/square-facade.d.ts +360 -9
  72. package/dist/square-migration.d.ts +24 -0
  73. package/dist/square-migration.js +211 -0
  74. package/dist/square-projections.d.ts +2 -42
  75. package/dist/square-projections.js +1 -37
  76. package/dist/square-sqlite.d.ts +59 -0
  77. package/dist/square-sqlite.js +702 -0
  78. package/dist/stream.d.ts +3 -10
  79. package/dist/stream.js +81 -52
  80. package/dist/views.d.ts +27 -95
  81. package/dist/views.js +217 -87
  82. package/dist/wake-eligibility.d.ts +15 -5
  83. package/dist/wake-eligibility.js +32 -16
  84. package/dist/wakes.d.ts +3 -3
  85. package/dist/wakes.js +31 -27
  86. package/guides/application-facade-delivery-evidence.md +200 -0
  87. package/guides/application-facade-design.md +95 -0
  88. package/guides/change-waits.md +4 -4
  89. package/guides/migration.md +77 -0
  90. package/guides/primitive-catch-consolidation.md +85 -0
  91. package/guides/primitive-history-consolidation.md +76 -0
  92. package/guides/primitive-status-consolidation.md +87 -0
  93. package/guides/primitive-storage-consolidation.md +128 -0
  94. package/guides/row-runtime-delivery-evidence.md +160 -0
  95. package/guides/row-storage-design.md +567 -0
  96. package/package.json +1 -1
  97. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  98. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  99. package/plugins/opencode/delivery.ts +11 -11
  100. package/plugins/opencode/index.ts +3 -3
  101. package/plugins/paseo/client/square-message.tsx +70 -52
  102. package/plugins/paseo/shared/message.ts +15 -0
  103. package/plugins/pi/square-pi.js +3 -0
  104. package/dist/activity.d.ts +0 -14
  105. package/dist/activity.js +0 -248
  106. package/dist/square-file-adapter.d.ts +0 -26
  107. package/dist/square-file-adapter.js +0 -85
  108. package/dist/square-state.d.ts +0 -6
  109. package/dist/square-state.js +0 -138
  110. package/dist/square-storage.d.ts +0 -14
  111. package/dist/square-storage.js +0 -188
  112. package/dist/square-wiring.d.ts +0 -33
  113. package/dist/square-wiring.js +0 -109
  114. package/dist/watch.d.ts +0 -8
  115. package/dist/watch.js +0 -252
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.70",
3
+ "version": "0.3.71",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.70",
3
+ "version": "0.3.71",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.70",
3
+ "version": "0.3.71",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,11 +1,11 @@
1
1
  import type { EvidenceRecord, NativeDeliveryEvidence, PresenceRecord } from '../../host-ledger.js';
2
- import type { WakeTransportPort } from '../../ports.js';
3
- import { nameKey, type SquareState } from '../../model.js';
4
- import { observeSessionPending, sessionInbox } from '../../inbox.js';
2
+ import type { SquareRead, WakeTransportPort } from '../../ports.js';
3
+ import { nameKey } from '../../model.js';
4
+ import { nativePendingPreview, observeSessionPending, sessionInbox } from '../../inbox.js';
5
5
  import { hostLedgerForEnv } from '../../registry.js';
6
- import { openSquare } from '../../square-file-adapter.js';
6
+ import { openSquare } from '../../open-square.js';
7
7
  import { closeOpenSquare } from '../../open-square.js';
8
- import { nativePendingPreview, presentationSuppressesWake, projectPresentationEvidence } from '../../square-projections.js';
8
+ import { presentationSuppressesWake, projectPresentationEvidence } from '../../square-projections.js';
9
9
  import { presentPending } from '../../presentation-operations.js';
10
10
  import { deliverPending } from '../../delivery-operations.js';
11
11
  import { createWakeTransport } from '../../notifications.js';
@@ -19,8 +19,8 @@ export interface OpenCodeContextMessage { readonly id?: string; readonly role: s
19
19
  const key = (location: string, participant: string, index: number) => `${location}\0${nameKey(participant)}\0${index}`;
20
20
 
21
21
  /** The one native pending preview: the audience/catch/epoch fence before send and inside presentation. */
22
- function preview(state: SquareState, owner: PresenceRecord, index: number) {
23
- return nativePendingPreview(state, { location: owner.location, routeKind, sessionId: owner.session, participant: owner.participant, actIndex: index, epoch: owner.epoch, address: { sessionId: owner.session }, cancelledThrough: owner.cancelledThrough, now: Date.now() });
22
+ function preview(facts: SquareRead, owner: PresenceRecord, index: number) {
23
+ return nativePendingPreview(facts, { location: owner.location, routeKind, sessionId: owner.session, participant: owner.participant, actIndex: index, epoch: owner.epoch, address: { sessionId: owner.session }, cancelledThrough: owner.cancelledThrough, now: Date.now() });
24
24
  }
25
25
 
26
26
  function createOpenCodeWakeTransport(target: OpenCodeNativeSession, env: NodeJS.ProcessEnv, lifetime: AbortSignal): WakeTransportPort {
@@ -41,13 +41,13 @@ function createOpenCodeWakeTransport(target: OpenCodeNativeSession, env: NodeJS.
41
41
  if (!owner || index === undefined) return undefined;
42
42
  const square = await openSquare(owner.location, { hostLedger: ledger, env, signal });
43
43
  try {
44
- const prepared = preview((await square.artifact.read(signal)).state, owner, index);
44
+ const { value: prepared } = await square.artifact.read((facts) => preview(facts, owner, index), signal);
45
45
  if (!prepared) return undefined;
46
46
  const row: NativeRow & { routeKind: typeof routeKind } = { location: owner.location, participant: owner.participant, session: target.sessionId,
47
47
  activity: request.activity, kind: 'wake', outcome: 'dispatching', routeKind, attemptN: request.attemptN, claimToken: request.claimToken,
48
48
  nativeDelivery: { harness: 'opencode', epoch: owner.epoch ?? 0, inputId: createNativeInputId(), payload: prepared.payload } };
49
49
  if (!await ledger.prepareNativeWake(row) || !await (beforeSend?.() ?? Promise.resolve(true)) || signal.aborted
50
- || preview((await square.artifact.read(signal)).state, owner, index)?.payload !== prepared.payload) return undefined;
50
+ || (await square.artifact.read((facts) => preview(facts, owner, index)?.payload, signal)).value !== prepared.payload) return undefined;
51
51
  return { sending: sendNativeText(target, prepared.payload, { inputId: row.nativeDelivery.inputId, delivery: 'steer',
52
52
  timeoutMs: Math.max(1, deadline - Date.now()), signal }) };
53
53
  } finally { await closeOpenSquare(square); }
@@ -78,13 +78,13 @@ export async function observeOpenCodeContext(sessionId: string, messages: readon
78
78
  if (!owner || index === undefined) continue;
79
79
  const square = await openSquare(row.location, { hostLedger: ledger, env, signal });
80
80
  try {
81
- const prepared = preview((await square.artifact.read(signal)).state, owner, index);
81
+ const { value: prepared } = await square.artifact.read((facts) => preview(facts, owner, index), signal);
82
82
  if (!prepared || prepared.payload !== row.nativeDelivery.payload) continue;
83
83
  const prior = await projectPresentationEvidence({ hostLedger: ledger, location: row.location, participant: row.participant, sessionId, activity: row.activity });
84
84
  if (presentationSuppressesWake(prior)) continue;
85
85
  await presentPending({ artifact: square.artifact, hostLedger: ledger, location: row.location, participant: row.participant, session: sessionId,
86
86
  activity: row.activity, sink: { present() {} }, markSeen: !prepared.clipped, signal,
87
- current: (state) => !signal.aborted && preview(state, owner, index)?.payload === row.nativeDelivery.payload });
87
+ current: (facts) => !signal.aborted && preview(facts, owner, index)?.payload === row.nativeDelivery.payload });
88
88
  } finally { await closeOpenSquare(square); }
89
89
  }
90
90
  }, signal);
@@ -1,9 +1,9 @@
1
1
  import { automaticSessionStart, operationEnv } from '../../automatic-session.js';
2
2
  import { hostLedgerForEnv } from '../../registry.js';
3
3
  import { canonicalPath } from '../../canonical-path.js';
4
- import { openSquare } from '../../square-file-adapter.js';
4
+ import { openSquare } from '../../open-square.js';
5
5
  import { closeOpenSquare } from '../../open-square.js';
6
- import { Square } from '../../square-wiring.js';
6
+ import { Square } from '../../square-application.js';
7
7
  import { connectNative, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
8
8
  import { observeOpenCodeContext, publishOpenCodeRoutes, receiveOpenCodePending, type OpenCodeContextMessage } from './delivery.js';
9
9
 
@@ -77,7 +77,7 @@ export default {
77
77
  await ledger.withClaimLock(async () => {
78
78
  for (const owner of (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode')) {
79
79
  const square = await openSquare(owner.location, { hostLedger: ledger, env, signal: lifetime.signal });
80
- try { await ledger.suppressPresence(owner, (await square.artifact.read(lifetime.signal)).state.runtime.nextActIndex - 1); }
80
+ try { await ledger.suppressPresence(owner, (await square.artifact.read((facts) => facts.header().nextActIndex - 1, lifetime.signal)).value); }
81
81
  finally { await closeOpenSquare(square); }
82
82
  }
83
83
  }, lifetime.signal);
@@ -1,8 +1,16 @@
1
1
  import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
2
  import { copyText, Icon, useToast } from "@getpaseo/plugin/client/react-native";
3
- import { useState } from "react";
3
+ import { useMemo, useState } from "react";
4
4
  import { Platform, Pressable, Text, View } from "react-native";
5
- import type { SquareMessageData } from "../shared/message";
5
+ import { squareHeadline, type SquareMessageData } from "../shared/message";
6
+
7
+ function withOpacity(color: string, opacity: number): string {
8
+ const hex = /^#([\da-f]{3}|[\da-f]{6})$/iu.exec(color);
9
+ if (hex === null) return color;
10
+ const value = hex[1]!.length === 3 ? [...hex[1]!].map((part) => part + part).join("") : hex[1]!;
11
+ const alpha = Math.round(opacity * 255).toString(16).padStart(2, "0");
12
+ return `#${value}${alpha}`;
13
+ }
6
14
 
7
15
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
8
16
  const [expanded, setExpanded] = useState(false);
@@ -11,77 +19,87 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
11
19
  const first = items?.[0];
12
20
  const muted = theme.colors.foregroundMuted;
13
21
  const foreground = theme.colors.foreground;
14
- const padding = layout.compact ? 12 : 16;
22
+ const padding = layout.compact ? 10 : 14;
23
+ const styles = useMemo(() => ({
24
+ marker: { color: theme.colors.accent, fontSize: 10, marginTop: 2 },
25
+ headline: { color: foreground, fontSize: 13, lineHeight: 19, flexShrink: 1 },
26
+ meta: { color: muted, fontSize: 11, lineHeight: 16, flexShrink: 1 },
27
+ bodyPanel: {
28
+ backgroundColor: theme.colors.surface1,
29
+ borderRadius: 12,
30
+ marginTop: 7,
31
+ marginLeft: 17,
32
+ overflow: "hidden" as const,
33
+ },
34
+ bodyText: {
35
+ color: foreground,
36
+ fontSize: 12,
37
+ lineHeight: 21,
38
+ fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
39
+ paddingHorizontal: layout.compact ? 12 : 14,
40
+ paddingTop: 12,
41
+ },
42
+ }), [expanded, foreground, layout.compact, muted, theme.colors.accent, theme.colors.surface1]);
15
43
 
16
44
  return (
17
- <View style={{
18
- backgroundColor: theme.colors.surface1,
19
- borderRadius: 10,
20
- overflow: "hidden",
21
- }}>
45
+ <View>
22
46
  <Pressable
23
47
  accessibilityRole="button"
24
- accessibilityLabel={first?.description ?? "Square context"}
48
+ accessibilityLabel={first !== undefined ? squareHeadline(first) : "Square context"}
25
49
  aria-expanded={expanded}
26
50
  onPress={() => setExpanded(value => !value)}
27
51
  style={({ pressed }) => ({
28
52
  flexDirection: "row",
29
- alignItems: "center",
30
- gap: 10,
31
- paddingHorizontal: padding,
32
- paddingVertical: 12,
33
- opacity: pressed ? 0.7 : 1,
53
+ alignItems: "flex-start",
54
+ gap: 7,
55
+ paddingVertical: 2,
56
+ opacity: pressed ? 0.6 : 1,
34
57
  })}
35
58
  >
36
- <Text style={{ color: theme.colors.accent, fontSize: 11 }}>■</Text>
59
+ <Text style={styles.marker}>■</Text>
37
60
  <View style={{
38
61
  flex: 1,
39
62
  minWidth: 0,
40
63
  flexDirection: layout.compact ? "column" : "row",
41
64
  alignItems: layout.compact ? "flex-start" : "center",
42
- gap: layout.compact ? 3 : 12,
65
+ gap: layout.compact ? 2 : 8,
43
66
  }}>
44
- <Text numberOfLines={layout.compact ? 2 : 1}
45
- style={{ color: foreground, fontSize: 13, fontWeight: "500", lineHeight: 19, flexShrink: 1 }}>
46
- {first?.description ?? "Square context"}
67
+ <Text numberOfLines={layout.compact ? 2 : 1} style={styles.headline}>
68
+ {first !== undefined ? squareHeadline(first) : "Square context"}
47
69
  </Text>
48
- {first && <Text numberOfLines={1} style={{ color: muted, fontSize: 11, lineHeight: 16, flexShrink: 1 }}>
70
+ {first && <Text numberOfLines={1} style={styles.meta}>
49
71
  {first.square}{items && items.length > 1 ? ` · +${items.length - 1} more` : ""}
50
72
  </Text>}
51
73
  </View>
52
- <Icon name={expanded ? "ChevronDown" : "ChevronRight"} size={14} color={muted} />
74
+ <View style={{ marginTop: 3 }}><Icon name={expanded ? "ChevronDown" : "ChevronRight"} size={14} color={muted} /></View>
53
75
  </Pressable>
54
- {expanded && <View style={{
55
- marginHorizontal: padding,
56
- borderTopWidth: 1,
57
- borderTopColor: theme.colors.border,
58
- paddingTop: 12,
59
- paddingBottom: 12,
60
- gap: 12,
61
- }}>
62
- <Text selectable style={{
63
- color: foreground,
64
- fontSize: 12,
65
- lineHeight: 20,
66
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
67
- }}>{text}</Text>
68
- <Pressable
69
- accessibilityRole="button"
70
- accessibilityLabel="Copy Square content"
71
- onPress={() => { void copyText(text).catch(() => toast.error("Could not copy Square content")); }}
72
- style={({ pressed }) => ({
73
- alignSelf: "flex-end",
74
- flexDirection: "row",
75
- alignItems: "center",
76
- gap: 5,
77
- paddingVertical: 4,
78
- paddingHorizontal: 4,
79
- opacity: pressed ? 0.6 : 1,
80
- })}
81
- >
82
- <Icon name="Copy" size={12} color={muted} />
83
- <Text style={{ color: muted, fontSize: 11 }}>Copy content</Text>
84
- </Pressable>
76
+ {expanded && <View style={styles.bodyPanel}>
77
+ <Text selectable style={styles.bodyText}>{text}</Text>
78
+ <View style={{
79
+ marginHorizontal: layout.compact ? 12 : 14,
80
+ marginTop: 12,
81
+ borderTopWidth: 1,
82
+ borderTopColor: withOpacity(theme.colors.border, 0.55),
83
+ paddingVertical: 7,
84
+ flexDirection: "row",
85
+ justifyContent: "flex-end",
86
+ }}>
87
+ <Pressable
88
+ accessibilityRole="button"
89
+ accessibilityLabel="Copy Square content"
90
+ onPress={() => { void copyText(text).catch(() => toast.error("Could not copy Square content")); }}
91
+ style={({ pressed }) => ({
92
+ flexDirection: "row",
93
+ alignItems: "center",
94
+ gap: 5,
95
+ paddingHorizontal: 2,
96
+ opacity: pressed ? 0.6 : 1,
97
+ })}
98
+ >
99
+ <Icon name="Copy" size={12} color={muted} />
100
+ <Text style={{ color: muted, fontSize: 11 }}>Copy content</Text>
101
+ </Pressable>
102
+ </View>
85
103
  </View>}
86
104
  </View>
87
105
  );
@@ -3,10 +3,25 @@ import { z } from "zod";
3
3
  export const squareCardDetailsSchema = z.object({
4
4
  items: z.array(z.object({
5
5
  square: z.string(),
6
+ actor: z.string().optional(),
7
+ recipient: z.string().optional(),
8
+ route: z.enum(["mention", "reply", "attention", "bell"]).optional(),
6
9
  description: z.string(),
7
10
  })).min(1),
8
11
  });
9
12
 
13
+ /** Human-facing summary; the stored description is Square's agent voice, and the human is not the recipient. */
14
+ export function squareHeadline(item: z.output<typeof squareCardDetailsSchema>["items"][number]): string {
15
+ if (item.actor === undefined || item.route === undefined) return item.description;
16
+ const to = item.recipient;
17
+ switch (item.route) {
18
+ case "mention": return to === undefined ? `${item.actor} mentioned you` : `${item.actor} mentioned ${to}`;
19
+ case "reply": return to === undefined ? `${item.actor} replied to you` : `${item.actor} replied to ${to}`;
20
+ case "attention": return `New from ${item.actor}`;
21
+ case "bell": return `${item.actor} called everyone`;
22
+ }
23
+ }
24
+
10
25
  export const squareMessageSchema = z.object({
11
26
  text: z.string(),
12
27
  items: squareCardDetailsSchema.shape.items.optional(),
@@ -35,6 +35,9 @@ function framePiMessage(content) {
35
35
  export function pendingCardDetails(pending) {
36
36
  const items = pending.flatMap((membership) => membership.notifications.map((note) => ({
37
37
  square: path.basename(membership.squarePath, '.square'),
38
+ actor: note.actor,
39
+ recipient: membership.name,
40
+ route: note.route,
38
41
  description: renderAttentionDescription({ actor: note.actor, recipient: membership.name, route: note.route }),
39
42
  })));
40
43
  return items.length === 0 ? undefined : { items };
@@ -1,14 +0,0 @@
1
- export interface ActivityOptions {
2
- cwd?: string;
3
- env?: NodeJS.ProcessEnv;
4
- force?: boolean;
5
- noWait?: boolean;
6
- noMention?: boolean;
7
- mentions?: readonly string[];
8
- reach?: import('./model.js').Reach;
9
- reply?: string;
10
- }
11
- export declare function saveActivityDraft(squarePath: string, name: string, body: string): string;
12
- /** Retry commands preserve the original reach, reply, wait and force choices. */
13
- export declare function activityRetryCommand(squarePath: string, name: string, opts: ActivityOptions, force?: boolean): string;
14
- export declare function cmdActivity(squarePath: string, name: string, activity: string, resolveBody: (arg: string) => string, opts: ActivityOptions): Promise<void>;
package/dist/activity.js DELETED
@@ -1,248 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { SquareError, isSquareError, validateName } from './model.js';
5
- import { expressHintLine, formatRefusal, renderActivityBlocked, renderActivityLimit, renderActivityUncertain, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, participantIdentity, participantsRecoveryCommand, participantCommandPrefix, commandPrefix, quoteShell, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
6
- import { nowMs, SLEEP_MS } from './runtime.js';
7
- import { unreadActivitySummaries } from './decisions.js';
8
- import { openSquare } from './square-file-adapter.js';
9
- import { closeOpenSquare } from './open-square.js';
10
- import { createSquareApplication } from './square-application.js';
11
- import { activityPresentation, resolveParticipant } from './views.js';
12
- import { formatActivityId, parseActivityId } from './square-core.js';
13
- import { formatDuration } from './time.js';
14
- import { style } from './tty-style.js';
15
- import { hostLedgerForEnv } from './registry.js';
16
- import { createDefaultWakeTransport } from './notifications.js';
17
- export function saveActivityDraft(squarePath, name, body) {
18
- const draftDir = path.join(path.dirname(squarePath), 'drafts');
19
- fs.mkdirSync(draftDir, { recursive: true });
20
- const hash = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
21
- const stamp = new Date(nowMs()).toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[:.]/g, '-');
22
- const namePart = name.replace(/[^\p{L}\p{N}_-]+/gu, '-').replace(/^-+|-+$/g, '') || 'participant';
23
- const base = `${stamp}-${namePart}-${hash}`;
24
- for (let i = 0;; i++) {
25
- const filename = `${base}${i === 0 ? '' : `-${i}`}.md`;
26
- const target = path.join(draftDir, filename);
27
- try {
28
- fs.writeFileSync(target, body, { flag: 'wx' });
29
- return target;
30
- }
31
- catch (error) {
32
- if (error.code !== 'EEXIST')
33
- throw error;
34
- }
35
- }
36
- }
37
- /** Retry commands preserve the original reach, reply, wait and force choices. */
38
- export function activityRetryCommand(squarePath, name, opts, force = opts.force ?? false) {
39
- const reach = opts.reach === 'bell' ? ' --bell'
40
- : opts.mentions?.length ? opts.mentions.map((target) => ` --mention ${quoteShell(target)}`).join('')
41
- : opts.noMention === true ? ' --no-mention'
42
- : '';
43
- const replyIndex = opts.reply === undefined ? undefined : parseActivityId(opts.reply);
44
- const reply = opts.reply === undefined ? '' : ` --reply ${replyIndex === undefined ? quoteShell(opts.reply) : formatActivityId(replyIndex)}`;
45
- return `${participantCommandPrefix(squarePath, name)} express${force ? ' --force' : ''}${opts.noWait ? ' --no-wait' : ''}${reach}${reply} -`;
46
- }
47
- /**
48
- * A recipient conflict is the speaker's to edit: the printed recovery drops every
49
- * mention flag (or keeps the bell) so the retry never repeats the rejected target.
50
- */
51
- function mentionRepairCommand(squarePath, name, opts, bell) {
52
- const replyIndex = opts.reply === undefined ? undefined : parseActivityId(opts.reply);
53
- const reply = opts.reply === undefined ? '' : ` --reply ${replyIndex === undefined ? quoteShell(opts.reply) : formatActivityId(replyIndex)}`;
54
- const reach = bell ? ' --bell' : ' --no-mention';
55
- return `${participantCommandPrefix(squarePath, name)} express${opts.force ? ' --force' : ''}${opts.noWait ? ' --no-wait' : ''}${reach}${reply} -`;
56
- }
57
- /** A body that is nothing but *gestures* reads as an action, not as speech. */
58
- function isGestureOnly(body) {
59
- return /\*[^*]*\*/.test(body) && body.replace(/\*[^*]*\*/g, '').trim() === '';
60
- }
61
- export async function cmdActivity(squarePath, name, activity, resolveBody, opts) {
62
- const rawInput = String(resolveBody(activity));
63
- const env = { ...(opts.env ?? process.env) };
64
- const hostLedger = hostLedgerForEnv(env);
65
- const retryCommand = activityRetryCommand(squarePath, name, opts);
66
- const forceCommand = activityRetryCommand(squarePath, name, opts, true);
67
- let knownName = name;
68
- let callerMissing = false;
69
- let expressAttempted = false;
70
- let landedId;
71
- let announcedWait;
72
- async function presentation() {
73
- const square = await openSquare(squarePath, { clock: nowMs, env, hostLedger });
74
- try {
75
- return await activityPresentation(square, knownName);
76
- }
77
- finally {
78
- await closeOpenSquare(square);
79
- }
80
- }
81
- try {
82
- validateName(name);
83
- for (const target of opts.mentions ?? [])
84
- validateName(target);
85
- if (opts.reply !== undefined && parseActivityId(opts.reply) === undefined) {
86
- throw new SquareError('invalid_args', 'Invalid --reply: expected an activity id like act/12');
87
- }
88
- const reader = await openSquare(squarePath, { clock: nowMs, env, hostLedger });
89
- try {
90
- knownName = (await resolveParticipant(reader, name)).name;
91
- }
92
- catch (error) {
93
- // The caller name was validated above; invalid_args here means it is absent
94
- // from the roster. Keep the join recovery distinct from recipient validation.
95
- callerMissing = isSquareError(error) && error.code === 'invalid_args';
96
- throw error;
97
- }
98
- finally {
99
- await closeOpenSquare(reader);
100
- }
101
- const application = createSquareApplication({ cwd: opts.cwd ?? process.cwd(), env, squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs), participant: name });
102
- // The caller is roster-known at this point, so a landed join act is always a
103
- // re-entry after done — never a first arrival. Announce it; a silent return
104
- // would make `done` a fake exit.
105
- const entry = await application.join();
106
- const reentered = entry.kind === 'joined' && entry.activity !== null;
107
- const before = await presentation();
108
- const pendingPublic = before.pendingPublic;
109
- expressAttempted = true;
110
- const landed = await application.express(rawInput.replace(/\r\n/g, '\n').trim(), {
111
- force: opts.force ?? false,
112
- noWait: opts.noWait ?? false,
113
- ...(opts.mentions === undefined ? {} : { mentions: opts.mentions }),
114
- ...(opts.noMention === undefined ? {} : { noMention: opts.noMention }),
115
- ...(opts.reach === undefined ? {} : { reach: opts.reach }),
116
- // The action boundary validates the raw id before attempting a commit.
117
- ...(opts.reply === undefined ? {} : { reply: opts.reply }),
118
- }, {
119
- onProgress: (progress) => {
120
- if (announcedWait === progress.reason)
121
- return;
122
- const delayMs = progress.delayMs ?? SLEEP_MS;
123
- process.stdout.write(renderExpressWaiting({
124
- reason: progress.reason,
125
- ...(progress.reason === 'throttled' ? { delayMs } : {}),
126
- ...(progress.holder === undefined ? {} : { holder: progress.holder }),
127
- ...(progress.holdReason === undefined ? {} : { holdReason: progress.holdReason }),
128
- }) + '\n');
129
- announcedWait = progress.reason;
130
- },
131
- });
132
- landedId = landed.activity.id;
133
- const fresh = await presentation();
134
- const headerCount = fresh.participantCount;
135
- const held = fresh.held;
136
- const ownActCount = fresh.ownActivityCount;
137
- const hasPending = pendingPublic.length > 0;
138
- const pendingSpeaker = pendingPublic[0]?.actor;
139
- const pendingLead = pendingSpeaker === undefined
140
- ? ''
141
- : `▲ while you spoke, ${participantIdentity(pendingSpeaker)} said something behind you\n\n`;
142
- const pending = hasPending ? `\n\n${pendingLead}${renderPendingFeed([...fresh.activities], [...pendingPublic], knownName, fresh.state)}` : '';
143
- const hint = expressHintLine(ownActCount);
144
- const receiptId = landed.activity.id;
145
- const capCount = fresh.hardCap === null ? '' : ` · ${ownActCount}/${fresh.hardCap}`;
146
- const confirmation = landed.activity.reach === 'bell'
147
- ? `● you ring the bell — everyone turns · ${receiptId}${style('dim', capCount)}`
148
- : `● ${landed.activity.mentions.length > 0
149
- ? `you walk over to ${landed.activity.mentions.map((target) => participantIdentity(target)).join(', ')}`
150
- : isGestureOnly(landed.activity.body ?? '')
151
- ? 'your gesture lands'
152
- : 'your words land'} — ${receiptId}${style('dim', capCount)}`;
153
- const withHint = hint ? `${confirmation}\n${style('dim', hint)}` : confirmation;
154
- const replyNotice = landed.replyAuthorNotHere === undefined
155
- ? ''
156
- : `\n○ ${participantIdentity(landed.replyAuthorNotHere)} isn't in the square right now — the reply landed without reaching them`;
157
- const reentry = reentered ? '● you stepped back into the square\n' : '';
158
- process.stdout.write(withPathOutput(squarePath, reentry + withHint + replyNotice + pending, { participantCount: headerCount, held }));
159
- }
160
- catch (error) {
161
- // Save before any recovery read: even a broken artifact must not eat the body.
162
- const draftPath = saveActivityDraft(squarePath, name, rawInput);
163
- const confirmedUnsent = landedId === undefined && (!expressAttempted || (isSquareError(error) && error.facts?.activityUnsent === true));
164
- const fresh = await presentation().catch(() => undefined);
165
- const output = { squarePath, name: knownName, draftPath, participantCount: fresh?.participantCount, held: fresh?.held };
166
- if (!confirmedUnsent) {
167
- process.stderr.write(renderActivityUncertain({ ...output, landedId, detail: error instanceof Error ? error.message : String(error) }));
168
- process.exit(1);
169
- return;
170
- }
171
- if (isSquareError(error) && fresh !== undefined) {
172
- if (error.code === 'behind') {
173
- process.stdout.write(renderActivityBlocked({
174
- ...output,
175
- retryCommand,
176
- forceCommand,
177
- activitySummaries: unreadActivitySummaries(fresh.state, knownName, nowMs()),
178
- }));
179
- process.exit(1);
180
- return;
181
- }
182
- if (error.code === 'capped') {
183
- process.stdout.write(renderActivityLimit({
184
- ...output,
185
- count: fresh.ownActivityCount,
186
- ...(fresh.hardCap === null ? {} : { hardCap: fresh.hardCap }),
187
- }));
188
- process.exit(1);
189
- return;
190
- }
191
- if (error.code === 'throttled' || error.code === 'held') {
192
- process.stdout.write(renderExpressNoWait({
193
- ...output,
194
- reason: error.code,
195
- delayMs: error.facts?.retryAfterMs ?? SLEEP_MS,
196
- ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
197
- ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
198
- retryCommand,
199
- }));
200
- process.exit(1);
201
- return;
202
- }
203
- if (error.code === 'bell_quota') {
204
- process.stdout.write(withPathOutput(squarePath, [
205
- '✕ the bell stays quiet for now',
206
- ` · you can ring it again in ${formatDuration(error.facts?.retryAfterMs ?? 1)}`,
207
- `· draft kept: ${draftPath}`,
208
- ` · retry when the bell is ready:`,
209
- `${retryCommand} < ${quoteShell(draftPath)}`,
210
- ].join('\n'), output));
211
- process.exit(1);
212
- return;
213
- }
214
- }
215
- const lines = [error instanceof Error ? error.message : String(error), `· draft kept: ${draftPath}`];
216
- const reason = isSquareError(error) ? error.facts?.reason : undefined;
217
- // `not_standing` is a flag mention; the body-scan and bell conflicts carry an @name in the draft.
218
- const mentionRepair = reason === 'bell_mention_conflict' ? { bell: true, fromBody: true }
219
- : reason === 'unmatched_mention' ? { bell: false, fromBody: true }
220
- : reason === 'not_standing' ? { bell: false, fromBody: false }
221
- : undefined;
222
- if (isSquareError(error)) {
223
- if (callerMissing || error.code === 'unknown_participant' || error.code === 'not_joined')
224
- lines.push(joinRecoveryCommand(squarePath, name));
225
- if (error.code === 'already_joined')
226
- lines.push(...takeoverRecoveryLines(squarePath, name));
227
- if (mentionRepair !== undefined) {
228
- // The draft stays, but the retry no longer carries the target that was refused.
229
- lines.push(participantsRecoveryCommand(squarePath));
230
- lines.push(mentionRepair.fromBody
231
- ? " · edit the draft first — the body's @name must be removed or wrapped in backticks:"
232
- : ' · pick someone standing here, or land it bare:');
233
- lines.push(`${mentionRepairCommand(squarePath, name, opts, mentionRepair.bell)} < ${quoteShell(draftPath)}`);
234
- }
235
- else if (error.code === 'invalid_args' || error.code === 'invalid_name') {
236
- if (opts.mentions?.length)
237
- lines.push(participantsRecoveryCommand(squarePath));
238
- if (opts.reply !== undefined)
239
- lines.push(`${commandPrefix(squarePath)} history --limit 10`);
240
- }
241
- }
242
- if (mentionRepair === undefined) {
243
- lines.push(' · correct the problem above before retrying; keep your intended recipients and reply:', `${retryCommand} < ${quoteShell(draftPath)}`);
244
- }
245
- process.stderr.write(formatRefusal(squarePath, lines, output));
246
- process.exit(2);
247
- }
248
- }
@@ -1,26 +0,0 @@
1
- export { observeSquareRevision as observeSquareChanges } from './artifact.js';
2
- import { type BuildOptions, type HardCap } from './model.js';
3
- import type { OpenSquare } from './open-square.js';
4
- import type { OperationControl } from './square-facade.js';
5
- import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
6
- /** File-owned artifact creation for the CLI and path-backed public facade. */
7
- export declare function createSquare(squarePath: string, options: BuildOptions & {
8
- hardCap: HardCap;
9
- }, snippet: string): Promise<void>;
10
- export interface SquareBuildOptions {
11
- markdown: string;
12
- hardCap?: number | null;
13
- throttlePerMinute?: number;
14
- clock?: () => number;
15
- hostLedger?: HostLedgerPort;
16
- wakeTransport?: import('./ports.js').WakeTransportPort;
17
- env?: NodeJS.ProcessEnv;
18
- /** One caller deadline (e.g. a native hook budget) propagated into artifact busy retries. */
19
- signal?: AbortSignal;
20
- }
21
- export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env' | 'signal'>, control?: OperationControl): Promise<OpenSquare>;
22
- export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
23
- export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
24
- export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
25
- /** A projection reads and validates its snapshot once; no preliminary duplicate read. */
26
- export declare function openSquareArtifact(squarePath: string, signal?: AbortSignal): SquareArtifactPort;