@astrosheep/square 0.3.10 → 0.3.12
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 +6 -7
- package/dist/artifact.js +337 -618
- package/dist/boundary-presentation.js +1 -1
- package/dist/cli/context.js +3 -3
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/maintenance-commands.js +12 -58
- package/dist/cli/observation-commands.js +14 -34
- package/dist/cli/program.js +3 -6
- package/dist/cli/registry.js +1 -2
- package/dist/cli/square-commands.js +39 -20
- package/dist/cmd/notify-once.js +5 -15
- package/dist/compact.js +4 -4
- package/dist/decisions.js +21 -7
- package/dist/delivery-health.js +56 -136
- package/dist/delivery.js +11 -47
- 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 +16 -18
- package/dist/index.js +11 -5
- package/dist/list.js +3 -47
- package/dist/model.js +4 -6
- package/dist/notifications.js +217 -32
- 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 +24 -39
- package/dist/presented.js +10 -72
- package/dist/registry.js +23 -24
- package/dist/routes.js +153 -0
- package/dist/runtime.js +6 -21
- package/dist/square-application.js +56 -127
- package/dist/square-core.js +56 -9
- package/dist/stream.js +1 -1
- package/dist/wake-attempts.js +175 -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 +7 -174
- package/package.json +6 -3
- package/skills/brainstorm/SKILL.md +28 -28
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +23 -14
- package/skills/square-feedback/SKILL.md +7 -7
- package/dist/doctor.js +0 -35
- package/dist/notification-failures.js +0 -54
- package/template.md +0 -4
- package/templates/architect.md +0 -4
- package/templates/brainstorm.md +0 -4
package/dist/delivery-health.js
CHANGED
|
@@ -1,143 +1,63 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
1
|
import { loadSquare } from './artifact.js';
|
|
5
|
-
import { deriveDeliveryModel } from './delivery.js';
|
|
6
|
-
import {
|
|
7
|
-
import { isCurrentlyJoined } from './runtime.js';
|
|
8
|
-
import { sameName } from './model.js';
|
|
2
|
+
import { deriveDeliveryModel, } from './delivery.js';
|
|
3
|
+
import { formatActivityId } from './square-core.js';
|
|
9
4
|
import { formatDuration } from './time.js';
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
export function deliveryLookbackMs(env = process.env) {
|
|
25
|
-
return positive('SQUARE_DELIVERY_LOOKBACK_MS', LOOKBACK_MS, env);
|
|
26
|
-
}
|
|
27
|
-
function actedAfter(doc, recipient, actIndex) {
|
|
28
|
-
return doc.acts.some((act) => act.actor !== undefined && sameName(act.actor, recipient) && act.index > actIndex);
|
|
29
|
-
}
|
|
30
|
-
function pending(squarePath, now) {
|
|
5
|
+
import { joinedRecipients, wakeEvidence } from './wake-evidence.js';
|
|
6
|
+
const DISPLAY_ORDER = [
|
|
7
|
+
'awaiting',
|
|
8
|
+
'wake-accepted',
|
|
9
|
+
'wake-unknown',
|
|
10
|
+
'presented-not-delivered',
|
|
11
|
+
'unreachable',
|
|
12
|
+
];
|
|
13
|
+
const ACTIONABLE = new Set(['wake-unknown', 'unreachable']);
|
|
14
|
+
/** Purely classify current pending attention from the artifact and durable ledgers. */
|
|
15
|
+
export function classifyDeliveryHealth(squarePath, opts) {
|
|
16
|
+
const now = opts.now ?? Date.now();
|
|
17
|
+
const env = opts.env ?? process.env;
|
|
31
18
|
const doc = loadSquare(squarePath);
|
|
32
19
|
const model = deriveDeliveryModel(doc);
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (recent.length > 0) {
|
|
75
|
-
const adapterFaults = recent.filter((item) => item.actedAfterWithoutDelivery);
|
|
76
|
-
out.push(adapterFaults.length > 0
|
|
77
|
-
? `✕ ${adapterFaults.length} pending notification(s) point to an adapter/pull dead path.`
|
|
78
|
-
: `✕ ${recent.length} recent notification(s) have no delivered receipt.`);
|
|
79
|
-
out.push(...byRecipient(recent));
|
|
20
|
+
return joinedRecipients(doc).flatMap((recipient) => model.pendingFor(recipient).map((note) => {
|
|
21
|
+
const ageMs = Math.max(0, now - note.item.at);
|
|
22
|
+
const evidence = wakeEvidence(squarePath, note.recipient, note.item.index, now, env);
|
|
23
|
+
const kind = evidence.presented
|
|
24
|
+
? 'presented-not-delivered'
|
|
25
|
+
: evidence.terminal?.outcome === 'accepted'
|
|
26
|
+
? 'wake-accepted'
|
|
27
|
+
: evidence.terminal?.outcome === 'unknown'
|
|
28
|
+
? 'wake-unknown'
|
|
29
|
+
: ageMs > opts.graceMs && evidence.attemptableRoutes.length === 0
|
|
30
|
+
? 'unreachable'
|
|
31
|
+
: 'awaiting';
|
|
32
|
+
const attempt = evidence.terminal ?? evidence.attempts.at(-1);
|
|
33
|
+
return {
|
|
34
|
+
squarePath,
|
|
35
|
+
recipient: note.recipient,
|
|
36
|
+
actIndex: note.item.index,
|
|
37
|
+
actor: note.item.actor,
|
|
38
|
+
at: note.item.at,
|
|
39
|
+
ageMs,
|
|
40
|
+
route: note.route,
|
|
41
|
+
kind,
|
|
42
|
+
...(attempt === undefined ? {} : { attempt }),
|
|
43
|
+
};
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
function formatItem(item) {
|
|
47
|
+
const evidence = item.attempt?.signature === undefined ? '' : ` · ${item.attempt.signature}`;
|
|
48
|
+
return ` · ${formatActivityId(item.actIndex)} → @${item.recipient} from @${item.actor} · ${formatDuration(item.ageMs)}${evidence}`;
|
|
49
|
+
}
|
|
50
|
+
export function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
|
|
51
|
+
const items = classifyDeliveryHealth(squarePath, { graceMs, now, env });
|
|
52
|
+
if (items.length === 0)
|
|
53
|
+
return ['✓ no pending delivery attention'];
|
|
54
|
+
const out = [`· delivery attention · ${items.length} pending`];
|
|
55
|
+
for (const kind of DISPLAY_ORDER) {
|
|
56
|
+
const group = items.filter((item) => item.kind === kind);
|
|
57
|
+
if (group.length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
out.push(`${ACTIONABLE.has(kind) ? '✕' : '○'} ${kind}: ${group.length}`);
|
|
60
|
+
out.push(...group.map(formatItem));
|
|
80
61
|
}
|
|
81
|
-
if (historical.length > 0) {
|
|
82
|
-
out.push(`○ ${historical.length} older pending notification(s) remain as historical backlog.`);
|
|
83
|
-
out.push(...byRecipient(historical));
|
|
84
|
-
if (opts.previousBacklog !== undefined) {
|
|
85
|
-
const delta = historical.length - opts.previousBacklog;
|
|
86
|
-
out.push(delta === 0 ? ' · backlog unchanged since last doctor.' : delta > 0 ? ` · backlog grew by ${delta} since last doctor.` : ` · backlog shrank by ${-delta} since last doctor.`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else if ((opts.previousBacklog ?? 0) > 0)
|
|
90
|
-
out.push(`○ backlog cleared (was ${opts.previousBacklog}).`);
|
|
91
62
|
return out;
|
|
92
63
|
}
|
|
93
|
-
function baselineFile(env) {
|
|
94
|
-
return env.SQUARE_DELIVERY_BASELINE ?? path.join(os.homedir(), '.square', 'delivery-baseline.json');
|
|
95
|
-
}
|
|
96
|
-
function baseline(squarePath, env) {
|
|
97
|
-
try {
|
|
98
|
-
return JSON.parse(fs.readFileSync(baselineFile(env), 'utf8'))[path.resolve(squarePath)]?.backlogCount;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
function writeBaseline(squarePath, backlogCount, env, at) {
|
|
105
|
-
const file = baselineFile(env);
|
|
106
|
-
let rows = {};
|
|
107
|
-
try {
|
|
108
|
-
rows = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
109
|
-
}
|
|
110
|
-
catch { }
|
|
111
|
-
rows[path.resolve(squarePath)] = { backlogCount, at };
|
|
112
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
113
|
-
fs.writeFileSync(file, `${JSON.stringify(rows, null, 2)}\n`, { mode: 0o600 });
|
|
114
|
-
}
|
|
115
|
-
function formatFailures(squarePath, recent, env) {
|
|
116
|
-
const failures = readNotificationFailures(squarePath, env);
|
|
117
|
-
if (failures.length === 0)
|
|
118
|
-
return [];
|
|
119
|
-
const pendingKeys = new Set(recent.map((item) => `${item.recipient}\0${item.actIndex}`));
|
|
120
|
-
const current = failures.filter((item) => item.recipient !== undefined && pendingKeys.has(`${item.recipient}\0${item.actIndex}`));
|
|
121
|
-
const rows = current.length > 0 ? current : failures;
|
|
122
|
-
const historical = current.length === 0;
|
|
123
|
-
const latest = rows.at(-1);
|
|
124
|
-
const diagnostic = latest.diagnostic;
|
|
125
|
-
return [
|
|
126
|
-
historical ? `○ ${rows.length} historical notification failure(s) retained: ${latest.message}` : `✕ ${rows.length} notification attempt(s) failed: ${latest.message}; receipt remains pending.`,
|
|
127
|
-
...(diagnostic?.passwordPresent === false ? [' · PASEO_PASSWORD absent; pass PASEO_PASSWORD to the Codex process.'] : []),
|
|
128
|
-
` · ${notificationFailuresPathForDisplay(squarePath, env)}`,
|
|
129
|
-
];
|
|
130
|
-
}
|
|
131
|
-
function notificationFailuresPathForDisplay(squarePath, env) {
|
|
132
|
-
return env.SQUARE_NOTIFICATION_FAILURES ?? path.join(path.dirname(squarePath), 'notification-failures.ndjsonl');
|
|
133
|
-
}
|
|
134
|
-
export function doctorDeliveryHealth(squarePath, now = Date.now(), env = process.env) {
|
|
135
|
-
const { recent, historical } = partitionPendingDeliveries(squarePath, { now, staleMs: deliveryStaleMs(env), lookbackMs: deliveryLookbackMs(env) });
|
|
136
|
-
const prior = baseline(squarePath, env);
|
|
137
|
-
writeBaseline(squarePath, historical.length, env, now);
|
|
138
|
-
return [
|
|
139
|
-
`· stale after ${formatDuration(deliveryStaleMs(env))} · scan window ${formatDuration(deliveryLookbackMs(env))}`,
|
|
140
|
-
...(recent.length === 0 && historical.length === 0 ? ['✓ no stale undelivered notifications'] : formatStaleDeliveryWarnings(recent, historical, { previousBacklog: prior })),
|
|
141
|
-
...formatFailures(squarePath, recent, env),
|
|
142
|
-
];
|
|
143
|
-
}
|
package/dist/delivery.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { findParticipantName, sameName, } from './model.js';
|
|
2
|
-
import {
|
|
2
|
+
import { audienceOf, formatActivityId, resolveAudience } from './square-core.js';
|
|
3
|
+
import { actId, isCurrentlyJoined, lastJoinIndex, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
3
4
|
export function notificationMessageId(squarePath, actIndex) {
|
|
4
|
-
return `square:${squarePath}
|
|
5
|
-
}
|
|
6
|
-
export function isPendingNotification(notification) {
|
|
7
|
-
return notification.route !== 'broadcast';
|
|
5
|
+
return `square:${squarePath}#${formatActivityId(actIndex)}`;
|
|
8
6
|
}
|
|
9
7
|
function canonicalRecipient(doc, name) {
|
|
10
8
|
return resolveRosterName(doc, name) ?? name;
|
|
@@ -32,16 +30,6 @@ export function recordDeliveredRuntime(runtime, recipient, actOrIndex, receipt)
|
|
|
32
30
|
export function markDeliveredDelivery(doc, name, actOrIndex, at = Date.now()) {
|
|
33
31
|
return recordDeliveredDelivery(doc, name, actOrIndex, { at });
|
|
34
32
|
}
|
|
35
|
-
function uniqueKnownMentions(body, roster) {
|
|
36
|
-
const recipients = [];
|
|
37
|
-
for (const mention of extractMentions(body)) {
|
|
38
|
-
const known = findParticipantName(roster, mention);
|
|
39
|
-
if (known !== undefined && !recipients.some((recipient) => sameName(recipient, known))) {
|
|
40
|
-
recipients.push(known);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return recipients;
|
|
44
|
-
}
|
|
45
33
|
/**
|
|
46
34
|
* Derive delivery behavior once from the parsed Square document.
|
|
47
35
|
* All consumers share these targets instead of reinterpreting artifact text or cursor state.
|
|
@@ -53,23 +41,9 @@ export function deriveDeliveryModel(doc) {
|
|
|
53
41
|
if (item.kind !== 'say')
|
|
54
42
|
return [];
|
|
55
43
|
const sayItem = item;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.filter((recipient) => !sameName(recipient, actor))
|
|
60
|
-
.map((recipient) => ({ item: sayItem, recipient, route: 'bell' }));
|
|
61
|
-
}
|
|
62
|
-
if (sayItem.reach !== undefined) {
|
|
63
|
-
const recipient = findParticipantName(roster, sayItem.reach.beside);
|
|
64
|
-
return recipient === undefined || sameName(recipient, actor)
|
|
65
|
-
? []
|
|
66
|
-
: [{ item: sayItem, recipient, route: 'beside' }];
|
|
67
|
-
}
|
|
68
|
-
const mentions = uniqueKnownMentions(sayItem.body, roster).filter((recipient) => !sameName(recipient, actor));
|
|
69
|
-
const recipients = mentions.length > 0
|
|
70
|
-
? mentions
|
|
71
|
-
: roster.filter((recipient) => !sameName(recipient, actor));
|
|
72
|
-
const route = mentions.length > 0 ? 'mention' : 'broadcast';
|
|
44
|
+
const audience = audienceOf(sayItem);
|
|
45
|
+
const recipients = resolveAudience(audience, roster).filter((recipient) => !sameName(recipient, sayItem.actor));
|
|
46
|
+
const route = audience.kind === 'bell' ? 'bell' : 'mention';
|
|
73
47
|
return recipients.map((recipient) => ({ item: sayItem, recipient, route }));
|
|
74
48
|
}
|
|
75
49
|
function pendingFor(requestedRecipient) {
|
|
@@ -82,19 +56,16 @@ export function deriveDeliveryModel(doc) {
|
|
|
82
56
|
for (const act of doc.acts) {
|
|
83
57
|
if (act.kind !== 'say')
|
|
84
58
|
continue;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (act.reach === undefined && extractMentions(act.body).length === 0)
|
|
59
|
+
const audience = audienceOf(act);
|
|
60
|
+
if (audience.kind === 'mentions' && audience.names.length === 0)
|
|
88
61
|
continue;
|
|
89
62
|
for (const planned of plan(act)) {
|
|
90
|
-
if (planned.route === 'broadcast')
|
|
91
|
-
continue;
|
|
92
63
|
const joinedAt = joinedAfter.get(planned.recipient);
|
|
93
64
|
if (joinedAt === undefined || act.index <= joinedAt)
|
|
94
65
|
continue;
|
|
95
66
|
if (isDeliveryDelivered(doc, planned.recipient, act.index))
|
|
96
67
|
continue;
|
|
97
|
-
pendingByRecipient.get(planned.recipient)?.push(
|
|
68
|
+
pendingByRecipient.get(planned.recipient)?.push(planned);
|
|
98
69
|
}
|
|
99
70
|
}
|
|
100
71
|
}
|
|
@@ -118,7 +89,7 @@ export function markDeliveredNotifications(doc, recipient, delivered, at = Date.
|
|
|
118
89
|
}
|
|
119
90
|
/** Canonical say-activity filter shared by catch selection and hook ownership. */
|
|
120
91
|
export function matchesCatchFilter(activity, filter) {
|
|
121
|
-
if (activity.
|
|
92
|
+
if (audienceOf(activity).kind === 'bell')
|
|
122
93
|
return true;
|
|
123
94
|
if (filter.participants !== undefined &&
|
|
124
95
|
!filter.participants.some((participant) => sameName(participant, activity.actor))) {
|
|
@@ -128,16 +99,9 @@ export function matchesCatchFilter(activity, filter) {
|
|
|
128
99
|
}
|
|
129
100
|
/** True only when the live catch's own filters would deliver this notification. */
|
|
130
101
|
export function leaseOwnsNotification(lease, notification) {
|
|
131
|
-
const recipient = notification.recipient;
|
|
132
|
-
if (notification.route === 'beside' && recipient === undefined)
|
|
133
|
-
return false;
|
|
134
102
|
return matchesCatchFilter({
|
|
135
103
|
actor: notification.actor,
|
|
136
104
|
body: notification.body,
|
|
137
|
-
|
|
138
|
-
? 'bell'
|
|
139
|
-
: notification.route === 'beside'
|
|
140
|
-
? { beside: recipient }
|
|
141
|
-
: undefined,
|
|
105
|
+
...(notification.route === 'bell' ? { reach: 'bell' } : {}),
|
|
142
106
|
}, lease.filter ?? {});
|
|
143
107
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
const lockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
6
|
+
const heldSyncLocks = new Set();
|
|
7
|
+
function ownerState(lockPath) {
|
|
8
|
+
let pid;
|
|
9
|
+
try {
|
|
10
|
+
pid = Number.parseInt(fs.readFileSync(lockPath, 'utf8').split('\n')[0], 10);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return 'unknown';
|
|
14
|
+
}
|
|
15
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
16
|
+
return 'unknown';
|
|
17
|
+
try {
|
|
18
|
+
process.kill(pid, 0);
|
|
19
|
+
return 'alive';
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return error.code === 'ESRCH' ? 'dead' : 'alive';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function createLock(lockPath) {
|
|
26
|
+
let fd;
|
|
27
|
+
try {
|
|
28
|
+
fd = fs.openSync(lockPath, 'wx', 0o600);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === 'EEXIST')
|
|
32
|
+
return undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const token = `${process.pid}\n${Date.now()}\n${randomUUID()}\n`;
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(fd, token, 'utf8');
|
|
38
|
+
return token;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
try {
|
|
42
|
+
fs.unlinkSync(lockPath);
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function reclaimLock(lockPath, staleMs) {
|
|
52
|
+
try {
|
|
53
|
+
const stale = Date.now() - fs.statSync(lockPath).mtimeMs > staleMs;
|
|
54
|
+
if (ownerState(lockPath) !== 'dead' && !stale)
|
|
55
|
+
return false;
|
|
56
|
+
fs.unlinkSync(lockPath);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return error.code === 'ENOENT';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function releaseLock(lockPath, token) {
|
|
64
|
+
try {
|
|
65
|
+
if (fs.readFileSync(lockPath, 'utf8') === token)
|
|
66
|
+
fs.unlinkSync(lockPath);
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
}
|
|
70
|
+
function prepare(lockPath) {
|
|
71
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
export function withFileLockSync(lockPath, options, fn) {
|
|
74
|
+
if (heldSyncLocks.has(lockPath))
|
|
75
|
+
throw new Error(`Reentrant file lock: ${lockPath}`);
|
|
76
|
+
prepare(lockPath);
|
|
77
|
+
let token;
|
|
78
|
+
while (token === undefined) {
|
|
79
|
+
token = createLock(lockPath);
|
|
80
|
+
if (token !== undefined)
|
|
81
|
+
break;
|
|
82
|
+
if (reclaimLock(lockPath, options.staleMs))
|
|
83
|
+
continue;
|
|
84
|
+
Atomics.wait(lockWait, 0, 0, options.retryMs);
|
|
85
|
+
}
|
|
86
|
+
heldSyncLocks.add(lockPath);
|
|
87
|
+
try {
|
|
88
|
+
return fn();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
heldSyncLocks.delete(lockPath);
|
|
92
|
+
releaseLock(lockPath, token);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function withFileLock(lockPath, options, fn) {
|
|
96
|
+
prepare(lockPath);
|
|
97
|
+
let token;
|
|
98
|
+
while (token === undefined) {
|
|
99
|
+
token = createLock(lockPath);
|
|
100
|
+
if (token !== undefined)
|
|
101
|
+
break;
|
|
102
|
+
if (reclaimLock(lockPath, options.staleMs))
|
|
103
|
+
continue;
|
|
104
|
+
await sleep(options.retryMs);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return await fn();
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
releaseLock(lockPath, token);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/harness-codex.js
CHANGED
|
@@ -12,7 +12,7 @@ const LEGACY_MARKETPLACES = ['astrosheep-square'];
|
|
|
12
12
|
export function codexMarketplaceRoot(homeDir) {
|
|
13
13
|
return path.join(homeDir, '.square', 'codex', 'marketplaces', CODEX_MARKETPLACE_NAME);
|
|
14
14
|
}
|
|
15
|
-
function configuredMarketplaceRoot(homeDir, configText) {
|
|
15
|
+
function configuredMarketplaceRoot(homeDir, configText, codexHomeDir) {
|
|
16
16
|
const lines = configText.split('\n');
|
|
17
17
|
const header = `[marketplaces.${CODEX_MARKETPLACE_NAME}]`;
|
|
18
18
|
const start = lines.findIndex((line) => line.trim() === header);
|
|
@@ -28,10 +28,10 @@ function configuredMarketplaceRoot(homeDir, configText) {
|
|
|
28
28
|
if (!match)
|
|
29
29
|
return undefined;
|
|
30
30
|
const value = match[1].startsWith('"') ? JSON.parse(match[1]) : match[1].slice(1, -1);
|
|
31
|
-
return path.isAbsolute(value) ? value : path.resolve(codexHome(homeDir), value);
|
|
31
|
+
return path.isAbsolute(value) ? value : path.resolve(codexHome(homeDir, codexHomeDir), value);
|
|
32
32
|
}
|
|
33
|
-
function activeMarketplaceRoot(homeDir, configText) {
|
|
34
|
-
return configuredMarketplaceRoot(homeDir, configText) ?? codexMarketplaceRoot(homeDir);
|
|
33
|
+
function activeMarketplaceRoot(homeDir, configText, codexHomeDir) {
|
|
34
|
+
return configuredMarketplaceRoot(homeDir, configText, codexHomeDir) ?? codexMarketplaceRoot(homeDir);
|
|
35
35
|
}
|
|
36
36
|
export function codexPluginRoot(homeDir, marketplaceRoot = codexMarketplaceRoot(homeDir)) {
|
|
37
37
|
return path.join(marketplaceRoot, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
@@ -39,12 +39,15 @@ export function codexPluginRoot(homeDir, marketplaceRoot = codexMarketplaceRoot(
|
|
|
39
39
|
export function codexPluginHooksPath(homeDir, marketplaceRoot = codexMarketplaceRoot(homeDir)) {
|
|
40
40
|
return path.join(codexPluginRoot(homeDir, marketplaceRoot), 'hooks', 'hooks.json');
|
|
41
41
|
}
|
|
42
|
-
function codexHome(homeDir
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
function codexHome(homeDir, codexHomeDir) {
|
|
43
|
+
const explicit = codexHomeDir?.trim();
|
|
44
|
+
return explicit ? path.resolve(explicit) : path.join(homeDir, '.codex');
|
|
45
|
+
}
|
|
46
|
+
export function codexHomeHooksPath(homeDir, codexHomeDir) { return path.join(codexHome(homeDir, codexHomeDir), 'hooks.json'); }
|
|
47
|
+
export function codexConfigPath(homeDir, codexHomeDir) { return path.join(codexHome(homeDir, codexHomeDir), 'config.toml'); }
|
|
48
|
+
function runCodex(homeDir, args, codexHomeDir) {
|
|
46
49
|
const result = spawnSync(process.env.SQUARE_CODEX_BIN || 'codex', args, {
|
|
47
|
-
encoding: 'utf8', env: { ...process.env, HOME: homeDir, CODEX_HOME: codexHome(homeDir) }, timeout: 30_000,
|
|
50
|
+
encoding: 'utf8', env: { ...process.env, HOME: homeDir, CODEX_HOME: codexHome(homeDir, codexHomeDir) }, timeout: 30_000,
|
|
48
51
|
});
|
|
49
52
|
if (result.error)
|
|
50
53
|
throw result.error;
|
|
@@ -77,10 +80,11 @@ function writeAtomic(file, text) {
|
|
|
77
80
|
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
78
81
|
fs.renameSync(temp, file);
|
|
79
82
|
}
|
|
80
|
-
export async function installCodexPlugin(homeDir, run = runCodex) {
|
|
81
|
-
const
|
|
83
|
+
export async function installCodexPlugin(homeDir, run = runCodex, codexHomeDir) {
|
|
84
|
+
const resolvedCodexHome = codexHome(homeDir, codexHomeDir);
|
|
85
|
+
const config = codexConfigPath(homeDir, resolvedCodexHome);
|
|
82
86
|
const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
83
|
-
const root = activeMarketplaceRoot(homeDir, current);
|
|
87
|
+
const root = activeMarketplaceRoot(homeDir, current, resolvedCodexHome);
|
|
84
88
|
const staged = stageReplacement(root, (stage) => {
|
|
85
89
|
const plugin = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
86
90
|
fs.cpSync(fileURLToPath(new URL('../codex-plugin/', import.meta.url)), plugin, { recursive: true });
|
|
@@ -92,8 +96,8 @@ export async function installCodexPlugin(homeDir, run = runCodex) {
|
|
|
92
96
|
const notes = [];
|
|
93
97
|
try {
|
|
94
98
|
writeAtomic(config, upsertTomlSectionKey(current, 'features', 'hooks', 'true'));
|
|
95
|
-
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'add', root, '--json']), 'marketplace install', true);
|
|
96
|
-
const installed = run(homeDir, ['plugin', 'add', CODEX_PLUGIN_ID, '--json']);
|
|
99
|
+
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'add', root, '--json'], resolvedCodexHome), 'marketplace install', true);
|
|
100
|
+
const installed = run(homeDir, ['plugin', 'add', CODEX_PLUGIN_ID, '--json'], resolvedCodexHome);
|
|
97
101
|
requireSuccess(installed, 'plugin install');
|
|
98
102
|
let installedPath;
|
|
99
103
|
try {
|
|
@@ -108,8 +112,8 @@ export async function installCodexPlugin(homeDir, run = runCodex) {
|
|
|
108
112
|
const pluginId = `${SQUARE_IDENTITY.pluginName}@${marketplace}`;
|
|
109
113
|
if (!current.includes(`[marketplaces.${marketplace}]`) && !current.includes(`[plugins."${pluginId}"]`))
|
|
110
114
|
continue;
|
|
111
|
-
requireSuccess(run(homeDir, ['plugin', 'remove', pluginId, '--json']), 'legacy plugin removal', true);
|
|
112
|
-
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json']), 'legacy marketplace removal', true);
|
|
115
|
+
requireSuccess(run(homeDir, ['plugin', 'remove', pluginId, '--json'], resolvedCodexHome), 'legacy plugin removal', true);
|
|
116
|
+
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json'], resolvedCodexHome), 'legacy marketplace removal', true);
|
|
113
117
|
notes.push(`retired ${pluginId}`);
|
|
114
118
|
}
|
|
115
119
|
staged.finalize();
|
|
@@ -120,25 +124,27 @@ export async function installCodexPlugin(homeDir, run = runCodex) {
|
|
|
120
124
|
throw error;
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
|
-
export async function uninstallCodexPlugin(homeDir, run = runCodex) {
|
|
124
|
-
const
|
|
127
|
+
export async function uninstallCodexPlugin(homeDir, run = runCodex, codexHomeDir) {
|
|
128
|
+
const resolvedCodexHome = codexHome(homeDir, codexHomeDir);
|
|
129
|
+
const config = codexConfigPath(homeDir, resolvedCodexHome);
|
|
125
130
|
const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
126
|
-
const root = activeMarketplaceRoot(homeDir, current);
|
|
127
|
-
requireSuccess(run(homeDir, ['plugin', 'remove', CODEX_PLUGIN_ID, '--json']), 'plugin removal', true);
|
|
128
|
-
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE_NAME, '--json']), 'marketplace removal', true);
|
|
131
|
+
const root = activeMarketplaceRoot(homeDir, current, resolvedCodexHome);
|
|
132
|
+
requireSuccess(run(homeDir, ['plugin', 'remove', CODEX_PLUGIN_ID, '--json'], resolvedCodexHome), 'plugin removal', true);
|
|
133
|
+
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE_NAME, '--json'], resolvedCodexHome), 'marketplace removal', true);
|
|
129
134
|
for (const marketplace of LEGACY_MARKETPLACES) {
|
|
130
|
-
requireSuccess(run(homeDir, ['plugin', 'remove', `${SQUARE_IDENTITY.pluginName}@${marketplace}`, '--json']), 'legacy plugin removal', true);
|
|
131
|
-
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json']), 'legacy marketplace removal', true);
|
|
135
|
+
requireSuccess(run(homeDir, ['plugin', 'remove', `${SQUARE_IDENTITY.pluginName}@${marketplace}`, '--json'], resolvedCodexHome), 'legacy plugin removal', true);
|
|
136
|
+
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json'], resolvedCodexHome), 'legacy marketplace removal', true);
|
|
132
137
|
}
|
|
133
138
|
fs.rmSync(root, { recursive: true, force: true });
|
|
134
|
-
fs.rmSync(codexHomeHooksPath(homeDir), { force: true });
|
|
135
|
-
return { paths: [root,
|
|
139
|
+
fs.rmSync(codexHomeHooksPath(homeDir, resolvedCodexHome), { force: true });
|
|
140
|
+
return { paths: [root, config, codexHomeHooksPath(homeDir, resolvedCodexHome)], notes: [] };
|
|
136
141
|
}
|
|
137
|
-
export async function doctorCodexPlugin(homeDir, run = runCodex) {
|
|
138
|
-
const
|
|
142
|
+
export async function doctorCodexPlugin(homeDir, run = runCodex, codexHomeDir) {
|
|
143
|
+
const resolvedCodexHome = codexHome(homeDir, codexHomeDir);
|
|
144
|
+
const config = codexConfigPath(homeDir, resolvedCodexHome);
|
|
139
145
|
const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
140
|
-
const root = activeMarketplaceRoot(homeDir, current);
|
|
141
|
-
const listed = run(homeDir, ['plugin', 'list', '--json']);
|
|
146
|
+
const root = activeMarketplaceRoot(homeDir, current, resolvedCodexHome);
|
|
147
|
+
const listed = run(homeDir, ['plugin', 'list', '--json'], resolvedCodexHome);
|
|
142
148
|
return [
|
|
143
149
|
/^hooks\s*=\s*true$/m.test(current) ? `✓ features.hooks=true in ${config}` : `○ features.hooks missing in ${config}`,
|
|
144
150
|
fs.existsSync(codexPluginHooksPath(homeDir, root)) ? `✓ Square plugin hooks ${codexPluginHooksPath(homeDir, root)}` : `○ Square plugin bundle missing ${root}`,
|
package/dist/harness-links.js
CHANGED
|
@@ -122,6 +122,3 @@ export function opencodeExtensionLink(homeDir = os.homedir()) {
|
|
|
122
122
|
kind: 'extension',
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
-
export function piExtensionLink(homeDir = os.homedir()) {
|
|
126
|
-
return { source: path.join(packageRoot(), 'extensions', 'square-pi.js'), target: path.join(homeDir, '.pi', 'agent', 'extensions', 'square.js'), kind: 'extension' };
|
|
127
|
-
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { SQUARE_IDENTITY } from './identity.js';
|
|
5
|
+
export function piPackageSource() {
|
|
6
|
+
return `npm:${SQUARE_IDENTITY.packageName}@${SQUARE_IDENTITY.packageVersion}`;
|
|
7
|
+
}
|
|
8
|
+
export function piPackageRoot(homeDir) {
|
|
9
|
+
return path.join(homeDir, '.pi', 'agent', 'npm', 'node_modules', ...SQUARE_IDENTITY.packageName.split('/'));
|
|
10
|
+
}
|
|
11
|
+
function runPi(homeDir, args) {
|
|
12
|
+
const result = spawnSync(process.env.SQUARE_PI_BIN || 'pi', args, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
env: { ...process.env, HOME: homeDir },
|
|
15
|
+
timeout: 30_000,
|
|
16
|
+
});
|
|
17
|
+
if (result.error)
|
|
18
|
+
throw result.error;
|
|
19
|
+
return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
20
|
+
}
|
|
21
|
+
function requireSuccess(result, action) {
|
|
22
|
+
if (result.status === 0)
|
|
23
|
+
return;
|
|
24
|
+
throw new Error(`Pi ${action} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
|
|
25
|
+
}
|
|
26
|
+
export function installPiPackage(homeDir, run = runPi) {
|
|
27
|
+
requireSuccess(run(homeDir, ['install', piPackageSource()]), 'package install');
|
|
28
|
+
return [piPackageRoot(homeDir)];
|
|
29
|
+
}
|
|
30
|
+
export function uninstallPiPackage(homeDir, run = runPi) {
|
|
31
|
+
requireSuccess(run(homeDir, ['remove', piPackageSource()]), 'package removal');
|
|
32
|
+
return [piPackageRoot(homeDir)];
|
|
33
|
+
}
|
|
34
|
+
export function doctorPiPackage(homeDir, run = runPi) {
|
|
35
|
+
const listed = run(homeDir, ['list']);
|
|
36
|
+
const root = piPackageRoot(homeDir);
|
|
37
|
+
const manifestPath = path.join(root, 'package.json');
|
|
38
|
+
let manifest;
|
|
39
|
+
try {
|
|
40
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// The diagnostics below name the missing or invalid package state.
|
|
44
|
+
}
|
|
45
|
+
const extensions = manifest?.pi?.extensions;
|
|
46
|
+
return [
|
|
47
|
+
listed.status === 0 && listed.stdout.includes(SQUARE_IDENTITY.packageName)
|
|
48
|
+
? `✓ Pi package ${SQUARE_IDENTITY.packageName} configured`
|
|
49
|
+
: `○ Pi package ${SQUARE_IDENTITY.packageName} not configured`,
|
|
50
|
+
manifest?.version === SQUARE_IDENTITY.packageVersion
|
|
51
|
+
? `✓ Pi package ${SQUARE_IDENTITY.packageVersion} installed at ${root}`
|
|
52
|
+
: `○ Pi package ${SQUARE_IDENTITY.packageVersion} missing at ${root}`,
|
|
53
|
+
Array.isArray(extensions) && extensions.includes('./extensions/square-pi.js')
|
|
54
|
+
? '✓ Pi Square extension declared'
|
|
55
|
+
: '○ Pi Square extension not declared',
|
|
56
|
+
];
|
|
57
|
+
}
|