@astrosheep/square 0.3.4 → 0.3.5

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
package/dist/activity.js CHANGED
@@ -4,10 +4,10 @@ import path from 'node:path';
4
4
  import { setTimeout as sleep } from 'node:timers/promises';
5
5
  import { loadSquare } from './artifact.js';
6
6
  import { SquareError, validateName } from './model.js';
7
- import { dispatchActNotifications } from './notifications.js';
8
7
  import { actHintLine, renderActivityBlocked, renderActivityLimit, renderActNoWait, renderActWaiting, renderPendingFeed, withActivityNextOutput, } from './presentation.js';
9
- import { appendAct, currentHold, inSquareCount, nowMs, SLEEP_MS, withSquareLock, resolveRosterName } from './runtime.js';
10
- import { decideAct, resolveKnownName } from './decisions.js';
8
+ import { currentHold, inSquareCount, nowMs, SLEEP_MS, resolveRosterName } from './runtime.js';
9
+ import { resolveKnownName } from './decisions.js';
10
+ import { execute } from './square-application.js';
11
11
  import { formatTimestamp } from './time.js';
12
12
  function draftDirFor(squarePath) {
13
13
  return path.join(path.dirname(squarePath), 'drafts');
@@ -68,19 +68,21 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
68
68
  : { beside: resolveKnownName(doc, opts.reach.beside) };
69
69
  let announcedWait;
70
70
  while (true) {
71
- const { decision, sent, headerCount, held } = await withSquareLock(squarePath, () => {
72
- const freshDoc = loadSquare(squarePath);
73
- const d = decideAct(freshDoc, { name, body, force, now: nowMs(), reach });
74
- if (d.type === 'sent') {
75
- const appended = appendAct(squarePath, freshDoc, d.act);
76
- return { decision: d, sent: { act: appended, index: appended.index }, headerCount: inSquareCount(freshDoc), held: currentHold(freshDoc.acts).active };
77
- }
78
- return { decision: d, sent: undefined, headerCount: inSquareCount(freshDoc), held: currentHold(freshDoc.acts).active };
71
+ const committed = await execute(squarePath, {
72
+ type: 'say',
73
+ name,
74
+ body,
75
+ force,
76
+ now: nowMs(),
77
+ ...(reach === undefined ? {} : { reach }),
79
78
  });
79
+ const decision = committed.result;
80
+ const sent = committed.acts[0];
81
+ const freshDoc = loadSquare(squarePath);
82
+ const headerCount = inSquareCount(freshDoc);
83
+ const held = currentHold(freshDoc.acts).active;
80
84
  switch (decision.type) {
81
85
  case 'sent': {
82
- if (sent)
83
- await dispatchActNotifications(squarePath, sent);
84
86
  const hasPending = decision.pendingPublic.length > 0 || decision.pendingRoomChanges.length > 0;
85
87
  const pending = hasPending ? `\n\n${renderPendingFeed(decision.pendingPublic, decision.pendingRoomChanges)}` : '';
86
88
  const hint = actHintLine(decision.ownActCount);
@@ -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, validateParticipantName } 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 === '-1')
69
+ return null;
70
+ if (!/^[1-9]\d*$/.test(value))
71
+ fail('Invalid build option: --cap must be a positive integer or -1.');
72
+ const parsed = Number(value);
73
+ if (!Number.isSafeInteger(parsed))
74
+ fail('Invalid build option: --cap must be a positive integer or -1.');
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
+ validateParticipantName(name);
83
+ return names;
84
+ }
85
+ export function requireParticipant(name) {
86
+ if (!name)
87
+ fail('Missing required option: --as <name>.');
88
+ validateParticipantName(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
+ validateParticipantName(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,92 @@
1
+ import fs from 'node:fs';
2
+ import { diagnoseSquare, loadSquare } from '../artifact.js';
3
+ import { doctorDeliveryHealth, findStalePendingMentions } from '../delivery-health.js';
4
+ import { renderDoctorClean, renderDoctorProblems, renderDoctorRepaired, renderDoctorUnfixable, withPathOutput, } from '../presentation.js';
5
+ import { inSquareCount } from '../runtime.js';
6
+ import { reconcileBacklog, repairSquare } from '../square-application.js';
7
+ import { SquareError } from '../model.js';
8
+ import { fail, 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
+ let reconcileBacklog = false;
26
+ for (let index = 0; index < argv.length; index++) {
27
+ const argument = argv[index];
28
+ if (argument === '--fix')
29
+ fix = true;
30
+ else if (argument === 'reconcile-backlog')
31
+ reconcileBacklog = true;
32
+ else if (argument === '--before') {
33
+ index += 1;
34
+ if (argv[index] === undefined)
35
+ usage(context.command);
36
+ }
37
+ else
38
+ usage(context.command);
39
+ }
40
+ if (reconcileBacklog && !fix)
41
+ fail('doctor reconcile-backlog requires --fix.');
42
+ return { fix, reconcileBacklog };
43
+ },
44
+ async execute(intent, context) {
45
+ if (!intent.fix) {
46
+ const diagnosis = diagnoseSquare(readSquareText(context.squarePath));
47
+ if (diagnosis.unfixable) {
48
+ return {
49
+ output: withPathOutput(context.squarePath, renderDoctorUnfixable(diagnosis.unfixable), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
50
+ exitCode: 2,
51
+ };
52
+ }
53
+ const summary = diagnosis.problems.length === 0 ? renderDoctorClean() : renderDoctorProblems(diagnosis.problems);
54
+ const delivery = doctorDeliveryHealth(context.squarePath).join('\n');
55
+ const stale = findStalePendingMentions(context.squarePath);
56
+ return {
57
+ output: withPathOutput(context.squarePath, `${summary}\n\n${delivery}`, { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
58
+ exitCode: diagnosis.problems.length === 0 && stale.length === 0 ? 0 : 1,
59
+ };
60
+ }
61
+ if (intent.reconcileBacklog) {
62
+ const result = await reconcileBacklog(context.squarePath);
63
+ const delivery = doctorDeliveryHealth(context.squarePath).join('\n');
64
+ return {
65
+ output: withPathOutput(context.squarePath, [
66
+ `✓ reconciled ${result.reconciled} backlog receipt(s) as delivered(reason=reconciled)`,
67
+ result.skippedRecent > 0 ? `· left ${result.skippedRecent} recent liveness failure(s) untouched` : '· no recent liveness failures present',
68
+ '',
69
+ delivery,
70
+ ].join('\n'), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
71
+ exitCode: result.skippedRecent > 0 ? 1 : 0,
72
+ };
73
+ }
74
+ const repair = await repairSquare(context.squarePath);
75
+ if (repair.diagnosis.unfixable) {
76
+ return {
77
+ output: withPathOutput(context.squarePath, renderDoctorUnfixable(repair.diagnosis.unfixable), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
78
+ exitCode: 2,
79
+ };
80
+ }
81
+ const repaired = repair.repaired;
82
+ const sidecar = quarantinePath(context.squarePath);
83
+ return {
84
+ output: withPathOutput(context.squarePath, renderDoctorRepaired(repaired.actions, repaired.quarantinedBlocks.length, repaired.quarantinedBlocks.length > 0 ? sidecar : undefined), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
85
+ };
86
+ },
87
+ present(result) {
88
+ process.stdout.write(result.output);
89
+ if (result.exitCode !== undefined)
90
+ process.exit(result.exitCode);
91
+ },
92
+ };
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { renderGlobalHelp, renderSubcommandHelp } from '../help.js';
4
+ import { fail } from './context.js';
5
+ export const helpCommand = {
6
+ parse(argv) {
7
+ if (argv.length > 1)
8
+ fail('Usage: square help [command]');
9
+ return { command: argv[0] };
10
+ },
11
+ execute(intent) {
12
+ if (intent.command === undefined)
13
+ return renderGlobalHelp();
14
+ const rendered = renderSubcommandHelp(intent.command);
15
+ if (rendered === undefined)
16
+ fail(`unknown command: ${intent.command}\nrun 'square help' to list every command`);
17
+ return rendered;
18
+ },
19
+ present: (result) => process.stdout.write(result),
20
+ };
21
+ export const versionCommand = {
22
+ parse() {
23
+ return undefined;
24
+ },
25
+ execute() {
26
+ const packagePath = fileURLToPath(new URL('../../package.json', import.meta.url));
27
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
28
+ return `${packageJson.version ?? 'unknown'}\n`;
29
+ },
30
+ present: (result) => process.stdout.write(result),
31
+ };