@astrosheep/square 0.3.9 → 0.3.11
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.
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +4 -0
- package/dist/artifact.js +33 -2
- package/dist/cli/context.js +1 -1
- package/dist/cli/maintenance-commands.js +12 -1
- package/dist/cli/observation-commands.js +3 -16
- package/dist/cli/program.js +0 -4
- package/dist/cli/square-commands.js +23 -3
- package/dist/cmd/notify-once.js +5 -15
- package/dist/decisions.js +10 -2
- package/dist/delivery-health.js +55 -136
- package/dist/doctor.js +1 -0
- package/dist/file-lock.js +112 -0
- package/dist/harness-codex.js +35 -29
- package/dist/harness-links.js +0 -3
- package/dist/harness-pi.js +57 -0
- package/dist/harness.js +10 -15
- package/dist/help.js +8 -8
- package/dist/index.js +5 -1
- package/dist/model.js +4 -0
- package/dist/notifications.js +205 -28
- package/dist/paseo-connection.js +135 -0
- package/dist/paseo-delivery.js +73 -144
- package/dist/paseo-state.js +1 -1
- package/dist/paseo-timeline.js +32 -42
- package/dist/presentation.js +2 -2
- package/dist/presented.js +10 -72
- package/dist/registry.js +23 -24
- package/dist/routes.js +153 -0
- package/dist/square-application.js +47 -49
- package/dist/stream.js +1 -1
- package/dist/wake-attempts.js +171 -0
- package/dist/wake-evidence.js +35 -0
- package/dist/wake-port.js +22 -0
- package/dist/wake-sink.js +45 -6
- package/dist/watch.js +1 -2
- package/guides/participant.md +1 -1
- package/package.json +6 -1
- package/skills/brainstorm/SKILL.md +24 -24
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +4 -3
- package/skills/square-feedback/SKILL.md +2 -2
- package/dist/notification-failures.js +0 -54
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { withFileLockSync } from './file-lock.js';
|
|
5
|
+
import { isWakeRouteKind, nameKey } from './model.js';
|
|
6
|
+
import { canonicalSquarePath } from './registry.js';
|
|
7
|
+
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
8
|
+
const LOCK_STALE_MS = 5 * 60 * 1000;
|
|
9
|
+
const LOCK_RETRY_MS = 10;
|
|
10
|
+
const VALID_OUTCOMES = new Set(['accepted', 'unknown', 'failed']);
|
|
11
|
+
export function wakeAttemptsPath(env = process.env) {
|
|
12
|
+
return env.SQUARE_WAKE_ATTEMPTS || path.join(os.homedir(), '.square', 'wake-attempts.ndjsonl');
|
|
13
|
+
}
|
|
14
|
+
export function wakeAttentionKey(attention) {
|
|
15
|
+
return JSON.stringify([canonicalSquarePath(attention.squarePath), `act_${attention.actIndex}`, nameKey(attention.recipient)]);
|
|
16
|
+
}
|
|
17
|
+
function parseRow(raw, now) {
|
|
18
|
+
let value;
|
|
19
|
+
try {
|
|
20
|
+
value = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
if (value === null || typeof value !== 'object')
|
|
26
|
+
return undefined;
|
|
27
|
+
const row = value;
|
|
28
|
+
if (row.v !== 1 || typeof row.ts !== 'number' || !Number.isFinite(row.ts) || row.ts > now || now - row.ts > RETENTION_MS ||
|
|
29
|
+
row.attention === undefined || typeof row.attention.square_path !== 'string' || row.attention.square_path === '' ||
|
|
30
|
+
typeof row.attention.act_id !== 'string' || !/^act_\d+$/.test(row.attention.act_id) ||
|
|
31
|
+
typeof row.attention.recipient !== 'string' || row.attention.recipient === '' ||
|
|
32
|
+
typeof row.outcome !== 'string' || !VALID_OUTCOMES.has(row.outcome) ||
|
|
33
|
+
typeof row.attempt_n !== 'number' || !Number.isInteger(row.attempt_n) || row.attempt_n <= 0 ||
|
|
34
|
+
!isWakeRouteKind(row.route_kind) ||
|
|
35
|
+
(row.signature !== undefined && typeof row.signature !== 'string') ||
|
|
36
|
+
(row.outcome !== 'accepted' && (typeof row.signature !== 'string' || row.signature === '')) ||
|
|
37
|
+
(row.message !== undefined && typeof row.message !== 'string'))
|
|
38
|
+
return undefined;
|
|
39
|
+
return row;
|
|
40
|
+
}
|
|
41
|
+
function readRowsFromFile(filePath, now) {
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code === 'ENOENT')
|
|
48
|
+
return [];
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return raw.split('\n').filter(Boolean).map((line) => parseRow(line, now)).filter((row) => row !== undefined);
|
|
52
|
+
}
|
|
53
|
+
function readRows(env, now) {
|
|
54
|
+
return readRowsFromFile(wakeAttemptsPath(env), now);
|
|
55
|
+
}
|
|
56
|
+
function writeRows(filePath, rows) {
|
|
57
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
58
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
59
|
+
fs.writeFileSync(temporary, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
|
|
60
|
+
mode: 0o600,
|
|
61
|
+
});
|
|
62
|
+
fs.renameSync(temporary, filePath);
|
|
63
|
+
}
|
|
64
|
+
function fromRow(row) {
|
|
65
|
+
return {
|
|
66
|
+
at: row.ts,
|
|
67
|
+
attention: {
|
|
68
|
+
squarePath: canonicalSquarePath(row.attention.square_path),
|
|
69
|
+
actIndex: Number(row.attention.act_id.slice(4)),
|
|
70
|
+
recipient: row.attention.recipient,
|
|
71
|
+
},
|
|
72
|
+
routeKind: row.route_kind,
|
|
73
|
+
outcome: row.outcome,
|
|
74
|
+
...(row.signature === undefined ? {} : { signature: row.signature }),
|
|
75
|
+
attemptN: row.attempt_n,
|
|
76
|
+
...(row.message === undefined ? {} : { message: row.message }),
|
|
77
|
+
...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function readWakeAttempts(opts = {}) {
|
|
81
|
+
const now = opts.now ?? Date.now();
|
|
82
|
+
const expected = opts.attention === undefined ? undefined : wakeAttentionKey(opts.attention);
|
|
83
|
+
return readRows(opts.env ?? process.env, now)
|
|
84
|
+
.map(fromRow)
|
|
85
|
+
.filter((attempt) => expected === undefined || wakeAttentionKey(attempt.attention) === expected);
|
|
86
|
+
}
|
|
87
|
+
export function terminalWakeEvidence(attempts) {
|
|
88
|
+
return attempts.findLast((attempt) => attempt.outcome === 'accepted' || attempt.outcome === 'unknown');
|
|
89
|
+
}
|
|
90
|
+
export function terminalWakeAttempt(attention, opts = {}) {
|
|
91
|
+
return terminalWakeEvidence(readWakeAttempts({ attention, ...opts }));
|
|
92
|
+
}
|
|
93
|
+
export function isWakeRouteAttemptable(route, attempts) {
|
|
94
|
+
if (terminalWakeEvidence(attempts) !== undefined)
|
|
95
|
+
return false;
|
|
96
|
+
const failed = attempts.findLast((attempt) => attempt.routeKind === route.kind && attempt.outcome === 'failed');
|
|
97
|
+
return failed === undefined || route.updatedAt > failed.at;
|
|
98
|
+
}
|
|
99
|
+
export function hasAttemptableWakeRoute(routes, attempts) {
|
|
100
|
+
return routes.some((route) => isWakeRouteAttemptable(route, attempts));
|
|
101
|
+
}
|
|
102
|
+
export function nextWakeAttemptNumber(attention, opts = {}) {
|
|
103
|
+
return readWakeAttempts({ attention, ...opts }).reduce((highest, attempt) => Math.max(highest, attempt.attemptN), 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
function redact(value, secret) {
|
|
106
|
+
if (typeof value === 'string') {
|
|
107
|
+
const withoutKnownSecret = secret ? value.split(secret).join('[redacted]') : value;
|
|
108
|
+
return withoutKnownSecret.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(value))
|
|
111
|
+
return value.map((item) => redact(item, secret));
|
|
112
|
+
if (value !== null && typeof value === 'object') {
|
|
113
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item, secret)]));
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function toRow(attempt, env) {
|
|
118
|
+
const safe = redact(attempt, env.PASEO_PASSWORD);
|
|
119
|
+
return {
|
|
120
|
+
v: 1,
|
|
121
|
+
ts: safe.at,
|
|
122
|
+
attention: {
|
|
123
|
+
square_path: canonicalSquarePath(safe.attention.squarePath),
|
|
124
|
+
act_id: `act_${safe.attention.actIndex}`,
|
|
125
|
+
recipient: safe.attention.recipient,
|
|
126
|
+
},
|
|
127
|
+
route_kind: safe.routeKind,
|
|
128
|
+
outcome: safe.outcome,
|
|
129
|
+
...(safe.signature === undefined ? {} : { signature: safe.signature }),
|
|
130
|
+
attempt_n: safe.attemptN,
|
|
131
|
+
...(safe.message === undefined ? {} : { message: safe.message }),
|
|
132
|
+
...(safe.diagnostic === undefined ? {} : { diagnostic: safe.diagnostic }),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function recordWakeAttempt(attempt, env = process.env) {
|
|
136
|
+
const value = { ...attempt, at: attempt.at ?? Date.now() };
|
|
137
|
+
if (!isWakeRouteKind(value.routeKind))
|
|
138
|
+
throw new Error('Wake attempts require a real adapter route kind.');
|
|
139
|
+
if (value.outcome !== 'accepted' && !value.signature) {
|
|
140
|
+
throw new Error(`${value.outcome} wake attempts require a transport signature.`);
|
|
141
|
+
}
|
|
142
|
+
const file = wakeAttemptsPath(env);
|
|
143
|
+
withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
|
|
144
|
+
writeRows(file, [...readRowsFromFile(file, value.at), toRow(value, env)]);
|
|
145
|
+
});
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
export function recordRecoveredUnknown(attention, lease, env = process.env, at = Date.now()) {
|
|
149
|
+
const routeKind = lease.routeKind;
|
|
150
|
+
if (lease.attemptN === undefined || !isWakeRouteKind(routeKind))
|
|
151
|
+
return undefined;
|
|
152
|
+
const file = wakeAttemptsPath(env);
|
|
153
|
+
return withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
|
|
154
|
+
const rows = readRowsFromFile(file, at);
|
|
155
|
+
const attempts = rows.map(fromRow).filter((attempt) => wakeAttentionKey(attempt.attention) === wakeAttentionKey(attention));
|
|
156
|
+
const terminal = terminalWakeEvidence(attempts);
|
|
157
|
+
if (terminal !== undefined)
|
|
158
|
+
return terminal;
|
|
159
|
+
const value = {
|
|
160
|
+
at,
|
|
161
|
+
attention,
|
|
162
|
+
routeKind,
|
|
163
|
+
outcome: 'unknown',
|
|
164
|
+
signature: 'worker_interrupted_during_dispatch',
|
|
165
|
+
attemptN: lease.attemptN,
|
|
166
|
+
message: 'The notification worker ended after dispatch began; transport acceptance is unknown.',
|
|
167
|
+
};
|
|
168
|
+
writeRows(file, [...rows, toRow(value, env)]);
|
|
169
|
+
return value;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { loadSquare } from './artifact.js';
|
|
2
|
+
import { isDeliveryDelivered } from './delivery.js';
|
|
3
|
+
import { hasPresentedAttention } from './presented.js';
|
|
4
|
+
import { lookupParticipant } from './registry.js';
|
|
5
|
+
import { isCurrentlyJoined } from './runtime.js';
|
|
6
|
+
import { readWakeRoutes } from './routes.js';
|
|
7
|
+
import { isWakeRouteAttemptable, readWakeAttempts, terminalWakeEvidence, } from './wake-attempts.js';
|
|
8
|
+
export function joinedRecipients(doc) {
|
|
9
|
+
return [...new Set(doc.acts.filter((act) => act.kind === 'join').map((act) => act.actor))]
|
|
10
|
+
.filter((name) => isCurrentlyJoined(doc.acts, name));
|
|
11
|
+
}
|
|
12
|
+
/** Project every wake decision from the same primary evidence. */
|
|
13
|
+
export function wakeEvidence(squarePath, recipient, actIndex, now, env) {
|
|
14
|
+
const doc = loadSquare(squarePath);
|
|
15
|
+
const owners = new Set(lookupParticipant(squarePath, recipient, now).map((binding) => binding.ownerId));
|
|
16
|
+
const attempts = readWakeAttempts({ attention: { squarePath, recipient, actIndex }, env, now });
|
|
17
|
+
const terminal = terminalWakeEvidence(attempts);
|
|
18
|
+
const routes = readWakeRoutes({ freshOnly: true, now, env })
|
|
19
|
+
.filter((route) => owners.has(route.ownerId));
|
|
20
|
+
return {
|
|
21
|
+
delivered: isDeliveryDelivered(doc, recipient, actIndex),
|
|
22
|
+
presented: hasPresentedAttention(squarePath, recipient, actIndex, env, now),
|
|
23
|
+
attempts,
|
|
24
|
+
...(terminal === undefined ? {} : { terminal }),
|
|
25
|
+
attemptableRoutes: terminal === undefined
|
|
26
|
+
? routes.filter((route) => isWakeRouteAttemptable(route, attempts))
|
|
27
|
+
: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function wakeIsEligible(evidence) {
|
|
31
|
+
return !evidence.delivered
|
|
32
|
+
&& !evidence.presented
|
|
33
|
+
&& evidence.terminal === undefined
|
|
34
|
+
&& evidence.attemptableRoutes.length > 0;
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Select routes globally; adapters own only transport-specific live proof and dispatch. */
|
|
2
|
+
export class WakePort {
|
|
3
|
+
adapters;
|
|
4
|
+
constructor(adapters) {
|
|
5
|
+
this.adapters = new Map(adapters.map((adapter) => [adapter.kind, adapter]));
|
|
6
|
+
}
|
|
7
|
+
async dispatch(routes, payload, hooks) {
|
|
8
|
+
for (const route of routes) {
|
|
9
|
+
const adapter = this.adapters.get(route.kind);
|
|
10
|
+
if (adapter === undefined)
|
|
11
|
+
continue;
|
|
12
|
+
const attemptN = hooks.nextAttemptN();
|
|
13
|
+
const result = await adapter.dispatch(route.address, payload, () => hooks.beforeSend(route, attemptN));
|
|
14
|
+
if (result.outcome === 'cancelled')
|
|
15
|
+
return result;
|
|
16
|
+
await hooks.record(route, attemptN, result);
|
|
17
|
+
if (result.outcome === 'accepted' || result.outcome === 'unknown')
|
|
18
|
+
return { outcome: result.outcome };
|
|
19
|
+
}
|
|
20
|
+
return { outcome: 'exhausted' };
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/wake-sink.js
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
export class PaseoWakeSendError extends Error {
|
|
3
|
+
kind;
|
|
4
|
+
constructor(message, kind) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.kind = kind;
|
|
7
|
+
this.name = 'PaseoWakeSendError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function commandError(output) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(output);
|
|
13
|
+
if (parsed.error === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
return {
|
|
16
|
+
...(typeof parsed.error.code === 'string' ? { code: parsed.error.code } : {}),
|
|
17
|
+
message: typeof parsed.error.message === 'string' ? parsed.error.message : output.trim(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function classifyCommandFailure(code, message) {
|
|
25
|
+
if (code === 'DAEMON_NOT_RUNNING' || /ECONNREFUSED|ENOENT|not found.*executable/i.test(message))
|
|
26
|
+
return 'transient';
|
|
27
|
+
if (/password|auth|unauthori[sz]ed|agent not found|rejected/i.test(message))
|
|
28
|
+
return 'rejected';
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
function redactUriPassword(value) {
|
|
32
|
+
return value.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
|
|
33
|
+
}
|
|
34
|
+
export function sendPaseoWake({ agentId, prompt }, opts = {}) {
|
|
35
|
+
const result = spawnSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['send', agentId, '--prompt', prompt, '--no-wait', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: opts.timeoutMs ?? 5000, env: process.env });
|
|
36
|
+
if (result.error) {
|
|
37
|
+
const code = result.error.code;
|
|
38
|
+
const kind = code === 'ENOENT' || code === 'ECONNREFUSED' ? 'transient' : 'unknown';
|
|
39
|
+
throw new PaseoWakeSendError(result.error.message, kind);
|
|
40
|
+
}
|
|
41
|
+
if (result.status === 0)
|
|
42
|
+
return;
|
|
43
|
+
const output = `${result.stderr ?? ''}${result.stdout ?? ''}`;
|
|
44
|
+
const failure = commandError(output);
|
|
45
|
+
const message = redactUriPassword(failure?.message || output.trim() || `paseo send exited with ${result.status ?? 'no status'}`);
|
|
46
|
+
throw new PaseoWakeSendError(message, classifyCommandFailure(failure?.code, message));
|
|
8
47
|
}
|
package/dist/watch.js
CHANGED
|
@@ -3,12 +3,11 @@ import { loadSquare } from './artifact.js';
|
|
|
3
3
|
import { SquareError, nameKey, } from './model.js';
|
|
4
4
|
import { markDeliveredNotifications } from './delivery.js';
|
|
5
5
|
import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, WATCH_STALE_MS, countSays, currentHold, doneNames, freshWatchLease, hasQuorum, inSquareCount, nowMs, touchPresenceCursor, writeWatchLease, } from './runtime.js';
|
|
6
|
-
import { withSquareLock, writeSquareDoc } from './square-application.js';
|
|
6
|
+
import { execute, withSquareLock, writeSquareDoc } from './square-application.js';
|
|
7
7
|
import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchOutput, renderWatchReplaced, renderWatchStatus, participantCommandPrefix, withPathOutput, } from './presentation.js';
|
|
8
8
|
import { ackPeerDelta, deliveryDelta, filteredPeerActivities, filteredRoomChanges, matchesFeedFilter, peerPublicActs, peerRoomChanges, } from './activity-feed.js';
|
|
9
9
|
import { coreParticipants, resolveKnownName } from './decisions.js';
|
|
10
10
|
import { hasAutomaticDeliveryIdentity, localParticipantOwner } from './registry.js';
|
|
11
|
-
import { execute } from './square-application.js';
|
|
12
11
|
function catchDelta(doc, name) {
|
|
13
12
|
return deliveryDelta(doc, name);
|
|
14
13
|
}
|
package/guides/participant.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
"哦对。一个名字只在广场上占一个位置。换了 harness session,再跑一次 `join` 会把新 session 接回原来的位置,不会制造第二个你。"
|
|
32
32
|
|
|
33
|
-
默认广场在 `.square/SQUARE.md`。不在?所有命令自己带 `--
|
|
33
|
+
默认广场在 `.square/SQUARE.md`。不在?所有命令自己带 `--location <path>`。别站着等人把路喂到你嘴边,恶心。
|
|
34
34
|
|
|
35
35
|
### 你的声音,和你的身体——express
|
|
36
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
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": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22",
|
|
41
|
+
"@types/ws": "^8.18.1",
|
|
41
42
|
"typescript": "^5.8"
|
|
42
43
|
},
|
|
43
44
|
"exports": {
|
|
@@ -55,5 +56,9 @@
|
|
|
55
56
|
"skills": [
|
|
56
57
|
"./skills"
|
|
57
58
|
]
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@getpaseo/client": "^0.3.1",
|
|
62
|
+
"ws": "^8.21.3"
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -7,7 +7,7 @@ allowed-tools: Bash(square *), Skill(square)
|
|
|
7
7
|
# Square Brainstorm
|
|
8
8
|
|
|
9
9
|
Use this skill when you are coordinating a brainstorm. Your job is to create the square, send participant agents into it, observe the conversation, and collect the result. Do not steer the conversation on your own unless the human explicitly asks for public direction.
|
|
10
|
-
Commands default to `.square/SQUARE.md`; use `--
|
|
10
|
+
Commands default to `.square/SQUARE.md`; use `--location <path>` when you want a different file.
|
|
11
11
|
|
|
12
12
|
## Build
|
|
13
13
|
|
|
@@ -37,27 +37,27 @@ Send each participant agent this prompt. Replace `<name>` and `<path>`, but do n
|
|
|
37
37
|
You are <name>, participating in a brainstorm. The square file is at <path>.
|
|
38
38
|
|
|
39
39
|
First action: enter the square. Read the context, warmup, and recent activity printed by this command before expressing:
|
|
40
|
-
square --
|
|
40
|
+
square --location <path> --as <name> join
|
|
41
41
|
|
|
42
42
|
Then follow the Happy Path from the join output. Core commands:
|
|
43
|
-
square --
|
|
43
|
+
square --location <path> --as <name> express - <<'EOF'
|
|
44
44
|
...
|
|
45
45
|
EOF
|
|
46
|
-
square --
|
|
47
|
-
square --
|
|
48
|
-
square --
|
|
49
|
-
square --
|
|
50
|
-
square --
|
|
51
|
-
square --
|
|
52
|
-
square --
|
|
46
|
+
square --location <path> --as <name> catch --mention --idle 10m
|
|
47
|
+
square --location <path> --as <name> catch --now
|
|
48
|
+
square --location <path> --as <name> catch --idle 10m
|
|
49
|
+
square --location <path> history --limit 80
|
|
50
|
+
square --location <path> history --from <name> --limit 80
|
|
51
|
+
square --location <path> status
|
|
52
|
+
square --location <path> --as <name> done - <<'EOF'
|
|
53
53
|
...
|
|
54
54
|
EOF
|
|
55
55
|
|
|
56
|
-
For complete history: square --
|
|
56
|
+
For complete history: square --location <path> history --all --full
|
|
57
57
|
|
|
58
58
|
If you are addressing a specific participant, write @name. Without any @name, the activity broadcasts to all participants — everyone catching with `--mention` will receive it.
|
|
59
59
|
|
|
60
|
-
If an activity is refused because something happened while the participant was not looking, run `square --
|
|
60
|
+
If an activity is refused because something happened while the participant was not looking, run `square --location <path> --as <name> catch --now`, take it in, then express again. `catch --now` catches up without waiting.
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Need another voice later? Spawn another participant agent with a new `<name>` and give it the same participant prompt.
|
|
@@ -67,12 +67,12 @@ Need another voice later? Spawn another participant agent with a new `<name>` an
|
|
|
67
67
|
If you or the human want to participate, choose a participant name and use the participant loop:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
square --
|
|
71
|
-
square --
|
|
70
|
+
square --location <path> --as <name> join
|
|
71
|
+
square --location <path> --as <name> express - <<'EOF'
|
|
72
72
|
your view
|
|
73
73
|
EOF
|
|
74
|
-
square --
|
|
75
|
-
square --
|
|
74
|
+
square --location <path> --as <name> catch --idle 10m
|
|
75
|
+
square --location <path> --as <name> done - <<'EOF'
|
|
76
76
|
final note
|
|
77
77
|
EOF
|
|
78
78
|
```
|
|
@@ -82,9 +82,9 @@ EOF
|
|
|
82
82
|
Use these to check progress:
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
square --
|
|
86
|
-
square --
|
|
87
|
-
square --
|
|
85
|
+
square --location <path> history --limit 50
|
|
86
|
+
square --location <path> history --from <name>
|
|
87
|
+
square --location <path> status
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
`history` reads past public activity without advancing participant presence. `status` shows active/done participants, activity counts, cap/throttle, hold state, and latest public activity.
|
|
@@ -96,7 +96,7 @@ When addressing a specific participant, use `@name`; without any `@name`, the ac
|
|
|
96
96
|
If the human wants to refocus the square, add a constraint, ask a convergence question, or correct its direction, write that direction publicly with a participant name:
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
square --
|
|
99
|
+
square --location <path> --as <name> express - <<'EOF'
|
|
100
100
|
Refocus on <specific direction, constraint, question, or decision needed>.
|
|
101
101
|
EOF
|
|
102
102
|
```
|
|
@@ -106,8 +106,8 @@ Do not add direction on your own. If you notice the square drifting or stuck, re
|
|
|
106
106
|
Pause the participant loop when a human needs time to read, think, or add another voice:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
square --
|
|
110
|
-
square --
|
|
109
|
+
square --location <path> hold "human reading"
|
|
110
|
+
square --location <path> resume
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
While held, participant expression and catch pause. Join, done, status, and history still work.
|
|
@@ -117,8 +117,8 @@ While held, participant expression and catch pause. Join, done, status, and hist
|
|
|
117
117
|
When participants are done, collect the public activities:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
square --
|
|
121
|
-
square --
|
|
120
|
+
square --location <path> history --all --full # complete public history
|
|
121
|
+
square --location <path> status
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
## Boundaries
|
package/skills/square/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ join once → catch ↔ express → done
|
|
|
19
19
|
square --as <name> join
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Read the current context and what happened recently before expressing. One name is one participant
|
|
22
|
+
Read the current context and what happened recently before expressing. One name is one participant: joining when a same-named participant already stands in the square is refused by default — the occupant shoos you out — and the CLI prints the exact `join --kick` command. `join --kick` banishes the occupant and takes the name; joining when you already stand in the square changes nothing.
|
|
23
23
|
|
|
24
24
|
## Express
|
|
25
25
|
|
|
@@ -58,13 +58,14 @@ Waiting with `catch --idle` is the normal way to be present between expressions;
|
|
|
58
58
|
|
|
59
59
|
## History
|
|
60
60
|
|
|
61
|
-
`history`
|
|
61
|
+
`history` is the only way to look back without changing what you have caught — remembering, not keeping up. Use `catch` to remain present.
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
square history --grep 'migration'
|
|
65
|
+
square history --all --full
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
See `square history --help` for filters.
|
|
68
|
+
See `square history --help` for filters. Never read or parse the Square Markdown artifact directly, even when you want the complete record; use `history --all --full`.
|
|
68
69
|
|
|
69
70
|
## Hold and step out
|
|
70
71
|
|
|
@@ -38,8 +38,8 @@ Use this template:
|
|
|
38
38
|
Run a nonblocking catch before expressing so the report does not land over unseen activity:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
square --
|
|
42
|
-
square --
|
|
41
|
+
square --location /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' catch --now
|
|
42
|
+
square --location /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' express - <<'EOF'
|
|
43
43
|
**Square feedback**
|
|
44
44
|
- Area: `catch --now`
|
|
45
45
|
- Square identity: `/absolute/path/to/project/.square/SQUARE-main.md (@participant)`
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
const MAX_BYTES = 1_000_000;
|
|
4
|
-
export function notificationFailuresPath(squarePath, env = process.env) {
|
|
5
|
-
return env.SQUARE_NOTIFICATION_FAILURES ?? path.join(path.dirname(squarePath), 'notification-failures.ndjsonl');
|
|
6
|
-
}
|
|
7
|
-
function redact(value, secret = process.env.PASEO_PASSWORD) {
|
|
8
|
-
if (typeof value === 'string')
|
|
9
|
-
return secret ? value.split(secret).join('[redacted]') : value;
|
|
10
|
-
if (Array.isArray(value))
|
|
11
|
-
return value.map((item) => redact(item, secret));
|
|
12
|
-
if (value !== null && typeof value === 'object') {
|
|
13
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item, secret)]));
|
|
14
|
-
}
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
function parseRows(file) {
|
|
18
|
-
try {
|
|
19
|
-
return fs.readFileSync(file, 'utf8').split('\n').flatMap((line) => {
|
|
20
|
-
if (line.trim() === '')
|
|
21
|
-
return [];
|
|
22
|
-
try {
|
|
23
|
-
const row = JSON.parse(line);
|
|
24
|
-
return row.v === 1 && row.op === 'failed' && typeof row.actIndex === 'number' && typeof row.sink === 'string' && typeof row.message === 'string' && typeof row.at === 'number'
|
|
25
|
-
? [{ actIndex: row.actIndex, recipient: row.recipient, route: row.route, sink: row.sink, message: row.message, at: row.at, ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }) }]
|
|
26
|
-
: [];
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return [];
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
if (error.code === 'ENOENT')
|
|
35
|
-
return [];
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export function readNotificationFailures(squarePath, env = process.env) {
|
|
40
|
-
return parseRows(notificationFailuresPath(squarePath, env));
|
|
41
|
-
}
|
|
42
|
-
/** Append diagnosable delivery failures without ever persisting Paseo credentials. */
|
|
43
|
-
export function recordNotificationFailure(squarePath, input, at = Date.now(), env = process.env) {
|
|
44
|
-
const file = notificationFailuresPath(squarePath, env);
|
|
45
|
-
const safe = redact({ ...input, at }, env.PASEO_PASSWORD);
|
|
46
|
-
const row = { v: 1, op: 'failed', ...safe };
|
|
47
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
48
|
-
const text = `${JSON.stringify(row)}\n`;
|
|
49
|
-
if (fs.existsSync(file) && fs.statSync(file).size + Buffer.byteLength(text) > MAX_BYTES) {
|
|
50
|
-
const retained = parseRows(file).slice(-500);
|
|
51
|
-
fs.writeFileSync(file, retained.map((item) => JSON.stringify({ v: 1, op: 'failed', ...item })).join('\n') + (retained.length ? '\n' : ''), { mode: 0o600 });
|
|
52
|
-
}
|
|
53
|
-
fs.appendFileSync(file, text, { mode: 0o600 });
|
|
54
|
-
}
|