@astrosheep/square 0.3.4 → 0.3.6

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 (52) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +23 -22
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +143 -0
  7. package/dist/cli/harness-command.js +50 -0
  8. package/dist/cli/maintenance-commands.js +76 -0
  9. package/dist/cli/meta-commands.js +28 -0
  10. package/dist/cli/observation-commands.js +453 -0
  11. package/dist/cli/program.js +48 -0
  12. package/dist/cli/registry.js +40 -0
  13. package/dist/cli/square-commands.js +219 -0
  14. package/dist/cmd/notify-once.js +23 -21
  15. package/dist/compact.js +6 -19
  16. package/dist/decisions.js +53 -86
  17. package/dist/delivery-health.js +104 -210
  18. package/dist/delivery.js +68 -18
  19. package/dist/doctor.js +9 -8
  20. package/dist/harness-claude.js +68 -0
  21. package/dist/harness-codex.js +119 -0
  22. package/dist/harness-links.js +123 -0
  23. package/dist/harness-stage.js +36 -0
  24. package/dist/harness.js +94 -576
  25. package/dist/help.js +44 -35
  26. package/dist/inbox.js +12 -11
  27. package/dist/index.js +30 -129
  28. package/dist/list.js +1 -1
  29. package/dist/model.js +0 -6
  30. package/dist/notification-failures.js +54 -0
  31. package/dist/notifications.js +47 -62
  32. package/dist/paseo-timeline.js +58 -188
  33. package/dist/presentation.js +55 -63
  34. package/dist/presented.js +9 -8
  35. package/dist/registry.js +55 -45
  36. package/dist/runtime.js +26 -137
  37. package/dist/square-application.js +264 -0
  38. package/dist/square-core.js +3 -11
  39. package/dist/square.js +5 -1362
  40. package/dist/stream.js +27 -126
  41. package/dist/wake-sink.js +134 -188
  42. package/dist/watch.js +79 -138
  43. package/extensions/square-opencode.js +1 -1
  44. package/extensions/square-pi.js +8 -130
  45. package/guides/architect.md +3 -3
  46. package/guides/participant.md +25 -16
  47. package/package.json +2 -2
  48. package/skills/brainstorm/SKILL.md +25 -32
  49. package/skills/square/.claude-plugin/plugin.json +1 -1
  50. package/skills/square/SKILL.md +39 -107
  51. package/skills/square-feedback/SKILL.md +4 -4
  52. package/dist/terminal.js +0 -125
@@ -1,12 +1,13 @@
1
1
  import { leaseOwnsNotification } from './delivery.js';
2
- import { notificationMessageId } from './delivery-health.js';
2
+ import { notificationMessageId } from './delivery.js';
3
3
  import { sessionInbox } from './inbox.js';
4
4
  import { participantCommandPrefix } from './presentation.js';
5
5
  import { presentOnce } from './presented.js';
6
6
  function pendingCount(inbox) {
7
7
  return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
8
8
  }
9
- const INJECT_BODY_MAX = 2048;
9
+ const INJECT_BODY_MAX = 200;
10
+ const INJECT_TOTAL_MAX = 1200;
10
11
  /** Let a fresh blocking catch own notifications it can deliver; hook injection remains the fallback. */
11
12
  export function deferToActiveCatch(inbox) {
12
13
  return inbox
@@ -16,38 +17,61 @@ export function deferToActiveCatch(inbox) {
16
17
  return membership;
17
18
  return {
18
19
  ...membership,
19
- notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, notification)),
20
+ notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })),
20
21
  };
21
22
  })
22
23
  .filter((membership) => membership.notifications.length > 0);
23
24
  }
24
- function injectBodyPreview(body, squarePath, name, actIndex) {
25
+ function injectBodyPreview(body) {
25
26
  const compact = body.replace(/\r\n/g, '\n');
26
27
  if (compact.length <= INJECT_BODY_MAX)
27
28
  return compact;
28
- const pointer = `${participantCommandPrefix(squarePath, name)} echo --ids act_${actIndex} --full`;
29
- return `${compact.slice(0, INJECT_BODY_MAX).trimEnd()}\n… [truncated] full echo: ${pointer}`;
29
+ return `${compact.slice(0, INJECT_BODY_MAX).trimEnd()}\n… [truncated; run catch --now]`;
30
30
  }
31
31
  export function renderClaudeInboxContext(inbox) {
32
32
  const count = pendingCount(inbox);
33
33
  const noun = count === 1 ? 'notification' : 'notifications';
34
- return [
35
- `<system-reminder source="square">You have ${count} unread Square ${noun}.`,
36
- ...inbox.flatMap((membership) => {
37
- const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
38
- return membership.notifications.map((notification) => {
39
- const id = notificationMessageId(membership.squarePath, notification.actIndex);
40
- const body = injectBodyPreview(notification.body, membership.squarePath, membership.name, notification.actIndex);
41
- return [
42
- `${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.via})`,
43
- body,
44
- `Ack with: ${command}`,
45
- ].join('\n');
46
- });
47
- }),
34
+ const header = `<system-reminder source="square">You have ${count} unread Square ${noun}.`;
35
+ const footer = [
48
36
  // Body here is a cache only. Delivered is written solely by catch.
49
37
  'Ids are stable across turns. If you already acted on an id, do not repeat the action; still run catch --now to mark delivered.',
50
38
  'Read and respond in the square before finishing the current turn.</system-reminder>',
39
+ ];
40
+ const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
41
+ const blocks = [];
42
+ let omitted = 0;
43
+ for (const [index, entry] of queued.entries()) {
44
+ const { membership, notification } = entry;
45
+ const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
46
+ const id = notificationMessageId(membership.squarePath, notification.actIndex);
47
+ const block = [
48
+ `${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.route})`,
49
+ injectBodyPreview(notification.body),
50
+ `Ack with: ${command}`,
51
+ ].join('\n');
52
+ const omittedAfter = omitted + queued.length - index - 1;
53
+ const prospective = [
54
+ header,
55
+ ...blocks,
56
+ block,
57
+ ...(omittedAfter > 0
58
+ ? [`… ${omittedAfter} unread ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
59
+ : []),
60
+ ...footer,
61
+ ].join('\n');
62
+ if (prospective.length > INJECT_TOTAL_MAX) {
63
+ omitted += 1;
64
+ continue;
65
+ }
66
+ blocks.push(block);
67
+ }
68
+ return [
69
+ header,
70
+ ...blocks,
71
+ ...(omitted > 0
72
+ ? [`… ${omitted} unread ${omitted === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
73
+ : []),
74
+ ...footer,
51
75
  ].join('\n');
52
76
  }
53
77
  function nativeHookResponse(input, lookup, env) {
@@ -58,7 +82,7 @@ function nativeHookResponse(input, lookup, env) {
58
82
  }
59
83
  if (input.hook_event_name === 'Stop' && input.stop_hook_active === true)
60
84
  return undefined;
61
- // Delivery membership is only claimed by explicit participant actions (join/act/catch/...).
85
+ // Delivery membership is only claimed by explicit participant actions (join/express/catch/...).
62
86
  // Inherited PASEO_AGENT_ID proves process ancestry, not conversational ownership.
63
87
  // Stop is the guaranteed "don't leave while undelivered" nudge; it does not consume presentation.
64
88
  if (input.hook_event_name === 'Stop') {
@@ -0,0 +1,143 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { loadSquare } from '../artifact.js';
5
+ import { commandUsageHint } from '../help.js';
6
+ import { parseParticipantList, validateName } from '../model.js';
7
+ export const DEFAULT_SQUARE_PATH = '.square/SQUARE.md';
8
+ export function readStdinSync() {
9
+ try {
10
+ return fs.readFileSync(0, 'utf8');
11
+ }
12
+ catch {
13
+ return '';
14
+ }
15
+ }
16
+ export function resolveBody(arg) {
17
+ return arg === '-' ? readStdinSync() : arg;
18
+ }
19
+ export function readPipedBodyFallback() {
20
+ if (process.stdin.isTTY)
21
+ return undefined;
22
+ const content = readStdinSync();
23
+ return content.trim() === '' ? undefined : content;
24
+ }
25
+ export function fail(message, exitCode = 2) {
26
+ process.stderr.write(`${message}\n`);
27
+ process.exit(exitCode);
28
+ }
29
+ export function usage(command) {
30
+ process.stderr.write(`✕ invalid arguments${command === '' ? '' : ` for ${command}`}\n`);
31
+ process.stderr.write(commandUsageHint(command || undefined));
32
+ process.exit(2);
33
+ }
34
+ export function requireValue(args, index, flag) {
35
+ const value = args[index + 1];
36
+ if (value === undefined || value.startsWith('--'))
37
+ fail(`Missing value for ${flag}.`);
38
+ return value;
39
+ }
40
+ export function parseDurationMs(value, flag) {
41
+ const match = value.match(/^([1-9]\d*)(ms|s|m|h)$/);
42
+ if (!match)
43
+ fail(`Invalid ${flag}: expected a positive duration with unit ms, s, m, or h (for example 500ms, 30s, 3m, 1h).`);
44
+ const amount = Number.parseInt(match[1], 10);
45
+ const multiplier = { ms: 1, s: 1000, m: 60000, h: 3600000 };
46
+ const milliseconds = amount * multiplier[match[2]];
47
+ if (!Number.isSafeInteger(milliseconds))
48
+ fail(`Invalid ${flag}: duration is too large.`);
49
+ return milliseconds;
50
+ }
51
+ export function parsePositiveInteger(value, flag) {
52
+ if (!/^[1-9]\d*$/.test(value))
53
+ fail(`Invalid ${flag}: expected a positive integer.`);
54
+ const parsed = Number(value);
55
+ if (!Number.isSafeInteger(parsed))
56
+ fail(`Invalid ${flag}: value is too large.`);
57
+ return parsed;
58
+ }
59
+ export function parseNonNegativeInteger(value, flag) {
60
+ if (!/^\d+$/.test(value))
61
+ fail(`Invalid ${flag}: expected a non-negative integer.`);
62
+ const parsed = Number(value);
63
+ if (!Number.isSafeInteger(parsed))
64
+ fail(`Invalid ${flag}: value is too large.`);
65
+ return parsed;
66
+ }
67
+ export function parseHardCap(value) {
68
+ if (value === 'unlimited')
69
+ return null;
70
+ if (!/^[1-9]\d*$/.test(value))
71
+ fail('Invalid build option: --cap must be a positive integer or unlimited.');
72
+ const parsed = Number(value);
73
+ if (!Number.isSafeInteger(parsed))
74
+ fail('Invalid build option: --cap must be a positive integer or unlimited.');
75
+ return parsed;
76
+ }
77
+ export function parseNameList(value, flag) {
78
+ const names = parseParticipantList(value);
79
+ if (names.length === 0)
80
+ fail(`Invalid ${flag}: expected at least one participant name.`);
81
+ for (const name of names)
82
+ validateName(name);
83
+ return names;
84
+ }
85
+ export function requireParticipant(name) {
86
+ if (!name)
87
+ fail('Missing required option: --as <name>.');
88
+ validateName(name);
89
+ return name;
90
+ }
91
+ function resolveDefaultSquarePath() {
92
+ const directory = path.join(process.cwd(), '.square');
93
+ let entries;
94
+ try {
95
+ entries = fs.readdirSync(directory, { withFileTypes: true });
96
+ }
97
+ catch {
98
+ return { path: DEFAULT_SQUARE_PATH, multiple: false };
99
+ }
100
+ const candidates = [];
101
+ for (const entry of entries) {
102
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
103
+ continue;
104
+ const fullPath = path.join(directory, entry.name);
105
+ try {
106
+ const doc = loadSquare(fullPath);
107
+ candidates.push({ relPath: path.relative(process.cwd(), fullPath), at: doc.acts.at(-1)?.at ?? fs.statSync(fullPath).mtimeMs });
108
+ }
109
+ catch {
110
+ // Unreadable artifacts are not implicit command targets.
111
+ }
112
+ }
113
+ candidates.sort((a, b) => b.at - a.at);
114
+ return { path: candidates[0]?.relPath ?? DEFAULT_SQUARE_PATH, multiple: candidates.length > 1 };
115
+ }
116
+ export function parseGlobalArgs(rawArgs) {
117
+ const args = [...rawArgs];
118
+ let requestedPath;
119
+ let name;
120
+ for (let index = 0; index < args.length; index++) {
121
+ if (args[index] === '--square-path') {
122
+ requestedPath = requireValue(args, index, args[index]);
123
+ args.splice(index, 2);
124
+ index -= 1;
125
+ }
126
+ else if (args[index] === '--as') {
127
+ name = requireValue(args, index, args[index]);
128
+ args.splice(index, 2);
129
+ index -= 1;
130
+ }
131
+ }
132
+ if (name !== undefined)
133
+ validateName(name);
134
+ const explicitSquarePath = requestedPath !== undefined;
135
+ const command = args[0];
136
+ const resolved = !explicitSquarePath && !['ls', 'list', 'version'].includes(command ?? '')
137
+ ? resolveDefaultSquarePath()
138
+ : { path: requestedPath ?? DEFAULT_SQUARE_PATH, multiple: false };
139
+ return { squarePath: resolved.path, explicitSquarePath, multipleSquares: resolved.multiple, name, args };
140
+ }
141
+ export function defaultContext(command, squarePath, name) {
142
+ return { command, squarePath, name, homeDir: os.homedir() };
143
+ }
@@ -0,0 +1,50 @@
1
+ import os from 'node:os';
2
+ import { executeHarnessTarget, harnessTargets, } from '../harness.js';
3
+ export const harnessCommand = {
4
+ parse(argv) {
5
+ const action = argv[0];
6
+ if (action !== 'install' && action !== 'uninstall' && action !== 'doctor') {
7
+ throw new Error('harness requires install, uninstall, or doctor');
8
+ }
9
+ const rest = argv.slice(1);
10
+ const option = rest.find((argument) => argument.startsWith('-') && argument !== '-f' && argument !== '--force');
11
+ if (option !== undefined)
12
+ throw new Error(`Unknown harness option: ${option}`);
13
+ return {
14
+ action,
15
+ target: rest.find((argument) => !argument.startsWith('-')),
16
+ force: rest.includes('-f') || rest.includes('--force'),
17
+ };
18
+ },
19
+ async execute(intent, context) {
20
+ if (intent.target === undefined && intent.action !== 'doctor') {
21
+ throw new Error('harness install/uninstall requires an explicit target: skills | claude | codex | opencode | pi');
22
+ }
23
+ const targetNames = intent.target === undefined
24
+ ? harnessTargets().map((target) => target.name)
25
+ : [intent.target];
26
+ const results = [];
27
+ for (const target of targetNames) {
28
+ results.push(await executeHarnessTarget(target, intent.action, {
29
+ homeDir: context.homeDir,
30
+ squarePath: context.squarePath,
31
+ force: intent.force,
32
+ }));
33
+ }
34
+ return {
35
+ notes: results.flatMap((result) => result.notes),
36
+ lines: results.flatMap((result) => result.lines),
37
+ };
38
+ },
39
+ present(result) {
40
+ process.stdout.write(formatHarnessResult(result));
41
+ },
42
+ };
43
+ export function formatHarnessResult(result) {
44
+ return `${[...result.notes, ...result.lines].join('\n')}\n`;
45
+ }
46
+ export function runHarnessCommand(argv, squarePath) {
47
+ const context = { homeDir: os.homedir(), squarePath: squarePath ?? '.square/SQUARE.md', command: 'harness' };
48
+ const intent = harnessCommand.parse(argv, context);
49
+ return Promise.resolve(harnessCommand.execute(intent, context)).then(formatHarnessResult);
50
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs';
2
+ import { diagnoseSquare, loadSquare } from '../artifact.js';
3
+ import { renderDoctorClean, renderDoctorProblems, renderDoctorRepaired, renderDoctorUnfixable, withPathOutput, } from '../presentation.js';
4
+ import { inSquareCount } from '../runtime.js';
5
+ import { repairSquare } from '../square-application.js';
6
+ import { SquareError } from '../model.js';
7
+ import { pruneRegistry } from '../registry.js';
8
+ import { usage } from './context.js';
9
+ function readSquareText(squarePath) {
10
+ try {
11
+ return fs.readFileSync(squarePath, 'utf8');
12
+ }
13
+ catch (error) {
14
+ if (error.code === 'ENOENT')
15
+ throw new SquareError('not_found', `square file not found: ${squarePath}`);
16
+ throw error;
17
+ }
18
+ }
19
+ function quarantinePath(squarePath) {
20
+ return squarePath.replace(/\.md$/, '') + '.quarantine.md';
21
+ }
22
+ export const doctorCommand = {
23
+ parse(argv, context) {
24
+ let fix = false;
25
+ for (let index = 0; index < argv.length; index++) {
26
+ const argument = argv[index];
27
+ if (argument === '--fix')
28
+ fix = true;
29
+ else if (argument === '--before') {
30
+ index += 1;
31
+ if (argv[index] === undefined)
32
+ usage(context.command);
33
+ }
34
+ else
35
+ usage(context.command);
36
+ }
37
+ return { fix };
38
+ },
39
+ async execute(intent, context) {
40
+ if (!intent.fix) {
41
+ const diagnosis = diagnoseSquare(readSquareText(context.squarePath));
42
+ if (diagnosis.unfixable) {
43
+ return {
44
+ output: withPathOutput(context.squarePath, renderDoctorUnfixable(diagnosis.unfixable), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
45
+ exitCode: 2,
46
+ };
47
+ }
48
+ const summary = diagnosis.problems.length === 0 ? renderDoctorClean() : renderDoctorProblems(diagnosis.problems);
49
+ return {
50
+ output: withPathOutput(context.squarePath, summary, { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
51
+ exitCode: diagnosis.problems.length === 0 ? 0 : 1,
52
+ };
53
+ }
54
+ const repair = await repairSquare(context.squarePath);
55
+ if (repair.diagnosis.unfixable) {
56
+ return {
57
+ output: withPathOutput(context.squarePath, renderDoctorUnfixable(repair.diagnosis.unfixable), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
58
+ exitCode: 2,
59
+ };
60
+ }
61
+ const repaired = repair.repaired;
62
+ const registry = pruneRegistry();
63
+ if (registry.removed > 0) {
64
+ repaired.actions.push({ message: `pruned ${registry.removed} obsolete registry membership(s)` });
65
+ }
66
+ const sidecar = quarantinePath(context.squarePath);
67
+ return {
68
+ output: withPathOutput(context.squarePath, renderDoctorRepaired(repaired.actions, repaired.quarantinedBlocks.length, repaired.quarantinedBlocks.length > 0 ? sidecar : undefined), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
69
+ };
70
+ },
71
+ present(result) {
72
+ process.stdout.write(result.output);
73
+ if (result.exitCode !== undefined)
74
+ process.exit(result.exitCode);
75
+ },
76
+ };
@@ -0,0 +1,28 @@
1
+ import { renderGlobalHelp, renderSubcommandHelp } from '../help.js';
2
+ import { SQUARE_IDENTITY } from '../identity.js';
3
+ import { fail } from './context.js';
4
+ export const helpCommand = {
5
+ parse(argv) {
6
+ if (argv.length > 1)
7
+ fail('Usage: square help [command]');
8
+ return { command: argv[0] };
9
+ },
10
+ execute(intent) {
11
+ if (intent.command === undefined)
12
+ return renderGlobalHelp();
13
+ const rendered = renderSubcommandHelp(intent.command);
14
+ if (rendered === undefined)
15
+ fail(`unknown command: ${intent.command}\nrun 'square help' to list available commands`);
16
+ return rendered;
17
+ },
18
+ present: (result) => process.stdout.write(result),
19
+ };
20
+ export const versionCommand = {
21
+ parse() {
22
+ return undefined;
23
+ },
24
+ execute() {
25
+ return `${SQUARE_IDENTITY.packageVersion}\n`;
26
+ },
27
+ present: (result) => process.stdout.write(result),
28
+ };