loadout-ai 0.7.0 → 0.9.0
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/CHANGELOG.md +148 -1
- package/README.md +160 -315
- package/SECURITY.md +21 -1
- package/catalog/discovered.json +31269 -26003
- package/catalog/packages.json +4 -4
- package/dist/src/cli.js +13 -0
- package/dist/src/commands/agents.js +3 -1
- package/dist/src/commands/catalog-candidate.js +150 -0
- package/dist/src/commands/catalog-workflows.js +147 -0
- package/dist/src/commands/catalog.js +9 -367
- package/dist/src/commands/coordinate.js +586 -0
- package/dist/src/commands/coordination-discussions.js +194 -0
- package/dist/src/commands/coordination-sessions.js +197 -0
- package/dist/src/commands/inventory.js +4 -2
- package/dist/src/core/agents/agent-inspection.js +26 -4
- package/dist/src/core/{routing → agents}/model-config.js +7 -2
- package/dist/src/core/catalog/catalog.js +1 -0
- package/dist/src/core/catalog/registry.js +44 -10
- package/dist/src/core/catalog/safety.js +36 -7
- package/dist/src/core/coordination/adapters/claude-code.js +154 -0
- package/dist/src/core/coordination/adapters/codex.js +137 -0
- package/dist/src/core/coordination/adapters/types.js +64 -0
- package/dist/src/core/coordination/auth.js +59 -0
- package/dist/src/core/coordination/bridge-lease.js +79 -0
- package/dist/src/core/coordination/conflict-preview.js +167 -0
- package/dist/src/core/coordination/contract-diff.js +106 -0
- package/dist/src/core/coordination/coordinator.js +552 -0
- package/dist/src/core/coordination/crash-recovery.js +169 -0
- package/dist/src/core/coordination/daemon.js +667 -0
- package/dist/src/core/coordination/discussion.js +340 -0
- package/dist/src/core/coordination/events.js +161 -0
- package/dist/src/core/coordination/http-api.js +118 -0
- package/dist/src/core/coordination/interrupt-policy.js +89 -0
- package/dist/src/core/coordination/lock.js +110 -0
- package/dist/src/core/coordination/mcp-server.js +424 -0
- package/dist/src/core/coordination/redaction.js +85 -0
- package/dist/src/core/coordination/replay.js +188 -0
- package/dist/src/core/coordination/retention.js +128 -0
- package/dist/src/core/coordination/runtime.js +21 -0
- package/dist/src/core/coordination/session-manager.js +366 -0
- package/dist/src/core/coordination/watcher.js +128 -0
- package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
- package/dist/src/core/{routing → delegation}/handoff.js +130 -63
- package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
- package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence.js +3 -214
- package/dist/src/core/discovery/community.js +12 -4
- package/dist/src/core/discovery/github-discovery.js +6 -2
- package/dist/src/core/discovery/private-discovery.js +6 -2
- package/dist/src/core/install/reconcile.js +1 -1
- package/dist/src/core/install/source.js +21 -7
- package/dist/src/core/install/uninstall.js +39 -3
- package/dist/src/core/reporting/cli-guide.js +10 -6
- package/dist/src/core/reporting/completion.js +61 -109
- package/dist/src/core/reporting/doctor.js +4 -6
- package/dist/src/core/runtime/bounded-json.js +65 -0
- package/dist/src/core/runtime/github.js +30 -16
- package/dist/src/core/runtime/mcp-recipes.js +1 -1
- package/dist/src/core/workspace/active-policy.js +1 -1
- package/docs/CANDIDATE_INTELLIGENCE.md +9 -2
- package/docs/CATALOG.md +1 -1
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +1 -1
- package/docs/DEMO_SCRIPT.md +19 -23
- package/docs/DISCOVERED.md +251 -250
- package/docs/FEATURE_TEST_MATRIX.md +10 -263
- package/docs/GITHUB_AUTHORIZATION.md +5 -0
- package/docs/LIVE_COLLABORATION.md +234 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +1 -1
- package/docs/REFERENCE.md +163 -0
- package/docs/RELEASE_REVIEW.md +1 -2
- package/docs/TESTING.md +17 -1
- package/docs/USER_TEST_GUIDE.md +138 -3
- package/docs/assets/loadout-discover-activate.webp +0 -0
- package/docs/assets/loadout-handoff-coordinate.webp +0 -0
- package/docs/assets/loadout-social-preview.png +0 -0
- package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
- package/docs/decisions/002-local-daemon-authentication.md +32 -0
- package/docs/decisions/003-bounded-agent-discussions.md +91 -0
- package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
- package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
- package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
- package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
- package/package.json +18 -7
- package/skills/loadout-handoff/SKILL.md +238 -0
- package/MASTER_PLAN.md +0 -2207
- package/dist/src/core/routing/route.js +0 -539
- package/docs/ACTIVE_SET.md +0 -53
- package/docs/COMPATIBILITY_POLICY.md +0 -22
- package/docs/CONVERSION_AND_SANDBOX.md +0 -27
- package/docs/EVALUATION_PROTOCOL_V1.md +0 -300
- package/docs/HEAD_TO_HEAD_EVALUATION.md +0 -79
- package/docs/PROVIDER_CONFIGURATION.md +0 -45
- package/docs/README_RESEARCH.md +0 -36
- package/docs/REPOSITORY_STABILIZATION.md +0 -190
- package/docs/SAFE_UPDATE_DEMO.md +0 -25
- package/docs/SCHEMA_DECISIONS.md +0 -25
- package/docs/SUBMISSION_COPY.md +0 -90
- package/docs/TEAM_POLICY.md +0 -18
- package/docs/assets/loadout-workflow.png +0 -0
- package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +0 -283
- package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +0 -116
- package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +0 -469
- package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +0 -80
- package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +0 -55
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +0 -228
- package/skills/loadout-router/SKILL.md +0 -120
- /package/dist/src/core/{routing → agents}/credentials.js +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redact sensitive data from coordination event payloads.
|
|
3
|
+
*
|
|
4
|
+
* Applied on emit, before storage. Patterns match common secret formats
|
|
5
|
+
* (API keys, tokens, passwords, GitHub PATs, Stripe keys, JWTs, etc.).
|
|
6
|
+
*/
|
|
7
|
+
const REDACTION_MARKER = "[REDACTED]";
|
|
8
|
+
const SENSITIVE_KEY = /^(?:api[_-]?key|access[_-]?token|auth(?:orization)?|bearer|client[_-]?secret|secret|token|password|passwd|pwd|credentials?)$/i;
|
|
9
|
+
const SECRET_PATTERNS = [
|
|
10
|
+
// Key-value patterns: api_key=..., secret: "...", password='...'
|
|
11
|
+
/(?:api[_-]?key|apikey|secret|token|password|passwd|pwd|credentials?)\s*[:=]\s*['"]?([^\s'"]{12,})['"]?/gi,
|
|
12
|
+
// Stripe keys: sk_live_..., pk_test_..., rk_live_...
|
|
13
|
+
/(?:sk|pk|rk)[-_](?:live|test)[-_][a-zA-Z0-9]{10,}/g,
|
|
14
|
+
// GitHub PATs
|
|
15
|
+
/ghp_[a-zA-Z0-9]{36}/g,
|
|
16
|
+
/github_pat_[a-zA-Z0-9_]{22,}/g,
|
|
17
|
+
// OpenAI keys
|
|
18
|
+
/sk-[a-zA-Z0-9]{20,}/g,
|
|
19
|
+
// Anthropic keys
|
|
20
|
+
/sk-ant-[a-zA-Z0-9_-]{20,}/g,
|
|
21
|
+
// AWS keys
|
|
22
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
23
|
+
// Bearer tokens
|
|
24
|
+
/Bearer\s+[a-zA-Z0-9._\-/+=]{20,}/gi,
|
|
25
|
+
// JWTs (three base64 segments separated by dots)
|
|
26
|
+
/eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g,
|
|
27
|
+
// Generic long hex/base64 strings that look like secrets (40+ chars)
|
|
28
|
+
/(?:^|["'\s=:])([a-f0-9]{40,})(?:["'\s]|$)/gm,
|
|
29
|
+
// .env style: ENV_VAR=value on its own line
|
|
30
|
+
/^[A-Z][A-Z0-9_]{2,}_(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)\s*=\s*(.+)$/gm,
|
|
31
|
+
];
|
|
32
|
+
export function redactString(input) {
|
|
33
|
+
let result = input;
|
|
34
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
35
|
+
// Reset lastIndex for stateful regexes
|
|
36
|
+
pattern.lastIndex = 0;
|
|
37
|
+
result = result.replace(pattern, (match) => {
|
|
38
|
+
// Keep the key part, redact the value part
|
|
39
|
+
const eqIndex = match.search(/[:=]\s*/);
|
|
40
|
+
if (eqIndex !== -1) {
|
|
41
|
+
return match.slice(0, eqIndex + 1) + " " + REDACTION_MARKER;
|
|
42
|
+
}
|
|
43
|
+
return REDACTION_MARKER;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export function redactPayload(payload) {
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
51
|
+
if (SENSITIVE_KEY.test(key) && typeof value === "string") {
|
|
52
|
+
result[key] = REDACTION_MARKER;
|
|
53
|
+
}
|
|
54
|
+
else if (typeof value === "string") {
|
|
55
|
+
result[key] = redactString(value);
|
|
56
|
+
}
|
|
57
|
+
else if (value !== null &&
|
|
58
|
+
typeof value === "object" &&
|
|
59
|
+
!Array.isArray(value)) {
|
|
60
|
+
result[key] = redactPayload(value);
|
|
61
|
+
}
|
|
62
|
+
else if (Array.isArray(value)) {
|
|
63
|
+
result[key] = value.map((item) => typeof item === "string"
|
|
64
|
+
? redactString(item)
|
|
65
|
+
: item !== null && typeof item === "object"
|
|
66
|
+
? redactPayload(item)
|
|
67
|
+
: item);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
result[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
export function redactDescription(description) {
|
|
76
|
+
return redactString(description);
|
|
77
|
+
}
|
|
78
|
+
export function redactCoordinationInput(input) {
|
|
79
|
+
return {
|
|
80
|
+
...input,
|
|
81
|
+
description: redactString(input.description),
|
|
82
|
+
...(input.context ? { context: redactString(input.context) } : {}),
|
|
83
|
+
...(input.payload ? { payload: redactPayload(input.payload) } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination replay — play back the full coordination timeline as a story.
|
|
3
|
+
*
|
|
4
|
+
* `loadout coord replay` reads the JSONL log and presents it as a formatted
|
|
5
|
+
* narrative timeline with timestamps, agent actions, and state transitions.
|
|
6
|
+
*/
|
|
7
|
+
import { readCoordLog, getContracts, getOwnership } from "./coordinator.js";
|
|
8
|
+
const TYPE_EMOJI = {
|
|
9
|
+
task: "📋",
|
|
10
|
+
handoff: "🤝",
|
|
11
|
+
question: "❓",
|
|
12
|
+
done: "✅",
|
|
13
|
+
status: "📊",
|
|
14
|
+
error: "❌",
|
|
15
|
+
cancel: "🚫",
|
|
16
|
+
contract: "📜",
|
|
17
|
+
ownership: "🔒",
|
|
18
|
+
decision: "⚖️",
|
|
19
|
+
update: "📝",
|
|
20
|
+
ack: "👍",
|
|
21
|
+
discussion: "💬",
|
|
22
|
+
};
|
|
23
|
+
function eventHeadline(event) {
|
|
24
|
+
const payload = event.payload;
|
|
25
|
+
switch (event.type) {
|
|
26
|
+
case "contract": {
|
|
27
|
+
const name = payload?.name ?? "unknown";
|
|
28
|
+
const rev = payload?.revision ?? "?";
|
|
29
|
+
return `Published contract "${name}" rev${rev}`;
|
|
30
|
+
}
|
|
31
|
+
case "ownership": {
|
|
32
|
+
const paths = payload?.paths ?? [];
|
|
33
|
+
const mode = payload?.mode ?? "exclusive";
|
|
34
|
+
return `Claimed ${mode} ownership of ${paths.length} path(s): ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? "..." : ""}`;
|
|
35
|
+
}
|
|
36
|
+
case "decision": {
|
|
37
|
+
const title = payload?.title ?? event.description;
|
|
38
|
+
return `Decision: ${title}`;
|
|
39
|
+
}
|
|
40
|
+
case "update": {
|
|
41
|
+
const note = payload?.note ?? event.description;
|
|
42
|
+
const files = payload?.files;
|
|
43
|
+
const filePart = files ? ` (${files.length} files)` : "";
|
|
44
|
+
return `${note}${filePart}`;
|
|
45
|
+
}
|
|
46
|
+
case "ack": {
|
|
47
|
+
const ackSeq = payload?.eventSeq ?? "?";
|
|
48
|
+
return `Acknowledged through seq ${ackSeq}`;
|
|
49
|
+
}
|
|
50
|
+
case "discussion": {
|
|
51
|
+
const threadId = payload?.threadId ?? "unknown";
|
|
52
|
+
const round = payload?.round ?? "?";
|
|
53
|
+
const kind = payload?.kind ?? "message";
|
|
54
|
+
return `Discussion ${threadId} · round ${round} · ${kind}`;
|
|
55
|
+
}
|
|
56
|
+
case "task":
|
|
57
|
+
return `Task: ${event.description}`;
|
|
58
|
+
case "handoff":
|
|
59
|
+
return `Handoff to ${event.to}: ${event.description}`;
|
|
60
|
+
case "done":
|
|
61
|
+
return `Completed: ${event.description}`;
|
|
62
|
+
case "error":
|
|
63
|
+
return `Error: ${event.description}`;
|
|
64
|
+
default:
|
|
65
|
+
return event.description;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function eventDetail(event) {
|
|
69
|
+
if (event.type !== "discussion" || !event.payload)
|
|
70
|
+
return event.context;
|
|
71
|
+
const payload = event.payload;
|
|
72
|
+
if (!payload.content)
|
|
73
|
+
return event.context;
|
|
74
|
+
return `${payload.content}${payload.replyTo ? ` (reply to ${payload.replyTo})` : ""}`;
|
|
75
|
+
}
|
|
76
|
+
function formatDuration(ms) {
|
|
77
|
+
if (ms < 1000)
|
|
78
|
+
return `${ms}ms`;
|
|
79
|
+
const seconds = Math.floor(ms / 1000);
|
|
80
|
+
if (seconds < 60)
|
|
81
|
+
return `${seconds}s`;
|
|
82
|
+
const minutes = Math.floor(seconds / 60);
|
|
83
|
+
if (minutes < 60)
|
|
84
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
85
|
+
const hours = Math.floor(minutes / 60);
|
|
86
|
+
return `${hours}h ${minutes % 60}m`;
|
|
87
|
+
}
|
|
88
|
+
function relativeTimestamp(eventTime, startTime) {
|
|
89
|
+
const ms = eventTime.getTime() - startTime.getTime();
|
|
90
|
+
return `+${formatDuration(ms)}`;
|
|
91
|
+
}
|
|
92
|
+
/** Max events to include in a replay to prevent memory issues. */
|
|
93
|
+
const MAX_REPLAY_EVENTS = 2000;
|
|
94
|
+
/**
|
|
95
|
+
* Build the replay timeline from the coordination log.
|
|
96
|
+
* Bounded to the most recent MAX_REPLAY_EVENTS entries.
|
|
97
|
+
*/
|
|
98
|
+
export async function buildReplay(projectRoot) {
|
|
99
|
+
const log = await readCoordLog(projectRoot);
|
|
100
|
+
const contracts = await getContracts(projectRoot);
|
|
101
|
+
const ownership = await getOwnership(projectRoot);
|
|
102
|
+
if (log.events.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
entries: [],
|
|
105
|
+
agents: [],
|
|
106
|
+
startTime: "",
|
|
107
|
+
endTime: "",
|
|
108
|
+
duration: "0s",
|
|
109
|
+
contractCount: 0,
|
|
110
|
+
ownershipCount: 0,
|
|
111
|
+
totalEvents: 0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Bound to recent events
|
|
115
|
+
const bounded = log.events.length > MAX_REPLAY_EVENTS
|
|
116
|
+
? log.events.slice(-MAX_REPLAY_EVENTS)
|
|
117
|
+
: log.events;
|
|
118
|
+
const startTime = new Date(bounded[0].timestamp);
|
|
119
|
+
const endTime = new Date(bounded[bounded.length - 1].timestamp);
|
|
120
|
+
const agents = [...new Set(bounded.map((e) => e.from))];
|
|
121
|
+
const entries = bounded.map((event) => ({
|
|
122
|
+
seq: event.seq,
|
|
123
|
+
timestamp: event.timestamp,
|
|
124
|
+
agent: event.from,
|
|
125
|
+
type: event.type,
|
|
126
|
+
emoji: TYPE_EMOJI[event.type] ?? "•",
|
|
127
|
+
headline: eventHeadline(event),
|
|
128
|
+
detail: eventDetail(event),
|
|
129
|
+
relativeTime: relativeTimestamp(new Date(event.timestamp), startTime),
|
|
130
|
+
}));
|
|
131
|
+
return {
|
|
132
|
+
entries,
|
|
133
|
+
agents,
|
|
134
|
+
startTime: bounded[0].timestamp,
|
|
135
|
+
endTime: bounded[bounded.length - 1].timestamp,
|
|
136
|
+
duration: formatDuration(endTime.getTime() - startTime.getTime()),
|
|
137
|
+
contractCount: contracts.size,
|
|
138
|
+
ownershipCount: ownership.size,
|
|
139
|
+
totalEvents: log.events.length,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Format replay timeline for terminal display.
|
|
144
|
+
*/
|
|
145
|
+
export function formatReplay(timeline) {
|
|
146
|
+
if (timeline.entries.length === 0) {
|
|
147
|
+
return "No coordination events to replay.";
|
|
148
|
+
}
|
|
149
|
+
const lines = [];
|
|
150
|
+
// Header
|
|
151
|
+
lines.push("\x1b[1m╔══════════════════════════════════════════╗\x1b[0m");
|
|
152
|
+
lines.push("\x1b[1m║ COORDINATION REPLAY ║\x1b[0m");
|
|
153
|
+
lines.push("\x1b[1m╚══════════════════════════════════════════╝\x1b[0m");
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push(` Agents: ${timeline.agents.map((a) => `\x1b[36m${a}\x1b[0m`).join(", ")}`);
|
|
156
|
+
lines.push(` Duration: ${timeline.duration}`);
|
|
157
|
+
lines.push(` Events: ${timeline.totalEvents} · Contracts: ${timeline.contractCount} · Ownership claims: ${timeline.ownershipCount}`);
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push("\x1b[90m─────────────────────────────────────────────\x1b[0m");
|
|
160
|
+
lines.push("");
|
|
161
|
+
// Assign colors to agents
|
|
162
|
+
const agentColors = ["\x1b[36m", "\x1b[33m", "\x1b[35m", "\x1b[34m"];
|
|
163
|
+
const colorMap = new Map();
|
|
164
|
+
timeline.agents.forEach((a, i) => {
|
|
165
|
+
colorMap.set(a, agentColors[i % agentColors.length]);
|
|
166
|
+
});
|
|
167
|
+
// Timeline entries
|
|
168
|
+
let lastAgent = "";
|
|
169
|
+
for (const entry of timeline.entries) {
|
|
170
|
+
const color = colorMap.get(entry.agent) ?? "\x1b[37m";
|
|
171
|
+
// Agent separator
|
|
172
|
+
if (entry.agent !== lastAgent) {
|
|
173
|
+
if (lastAgent)
|
|
174
|
+
lines.push("");
|
|
175
|
+
lastAgent = entry.agent;
|
|
176
|
+
}
|
|
177
|
+
const timeStr = `\x1b[90m${entry.relativeTime.padStart(8)}\x1b[0m`;
|
|
178
|
+
const agentStr = `${color}${entry.agent.padEnd(12)}\x1b[0m`;
|
|
179
|
+
lines.push(` ${timeStr} ${entry.emoji} ${agentStr} ${entry.headline}`);
|
|
180
|
+
if (entry.detail) {
|
|
181
|
+
lines.push(` \x1b[90m└─ ${entry.detail}\x1b[0m`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push("\x1b[90m─────────────────────────────────────────────\x1b[0m");
|
|
186
|
+
lines.push(` \x1b[32m✓\x1b[0m Replay complete · ${timeline.totalEvents} events`);
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention and compaction for the coordination log.
|
|
3
|
+
*
|
|
4
|
+
* The JSONL audit log is never deleted. Compaction rewrites the working log
|
|
5
|
+
* with a summary event followed by recent events, keeping the active set
|
|
6
|
+
* bounded.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, writeFile, rename, stat, mkdir } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { assertCoordinationEnabled, readCoordLog } from "./coordinator.js";
|
|
11
|
+
import { withCoordinationLock } from "./lock.js";
|
|
12
|
+
const COORD_DIR = ".handoff";
|
|
13
|
+
const COORD_LOG = "coordination.jsonl";
|
|
14
|
+
export const DEFAULT_RETENTION = {
|
|
15
|
+
maxEvents: 10000,
|
|
16
|
+
maxAgeDays: 30,
|
|
17
|
+
};
|
|
18
|
+
export async function compact(projectRoot, config = DEFAULT_RETENTION) {
|
|
19
|
+
const dir = join(projectRoot, COORD_DIR);
|
|
20
|
+
await mkdir(dir, { recursive: true });
|
|
21
|
+
return withCoordinationLock(dir, async () => {
|
|
22
|
+
await assertCoordinationEnabled(projectRoot);
|
|
23
|
+
const log = await readCoordLog(projectRoot);
|
|
24
|
+
if (!Number.isSafeInteger(config.maxEvents) || config.maxEvents < 1) {
|
|
25
|
+
throw new Error("maxEvents must be a positive integer");
|
|
26
|
+
}
|
|
27
|
+
if (!Number.isFinite(config.maxAgeDays) || config.maxAgeDays < 0) {
|
|
28
|
+
throw new Error("maxAgeDays must be a non-negative number");
|
|
29
|
+
}
|
|
30
|
+
const cutoffDate = new Date();
|
|
31
|
+
cutoffDate.setDate(cutoffDate.getDate() - config.maxAgeDays);
|
|
32
|
+
const cutoffIso = cutoffDate.toISOString();
|
|
33
|
+
const firstFresh = log.events.findIndex((event) => event.timestamp >= cutoffIso);
|
|
34
|
+
const ageStart = firstFresh === -1 ? log.events.length : firstFresh;
|
|
35
|
+
const countStart = Math.max(0, log.events.length - config.maxEvents);
|
|
36
|
+
const keepStart = Math.max(ageStart, countStart);
|
|
37
|
+
const remove = log.events.slice(0, keepStart);
|
|
38
|
+
const finalRetained = log.events.slice(keepStart);
|
|
39
|
+
if (remove.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
before: log.events.length,
|
|
42
|
+
after: log.events.length,
|
|
43
|
+
removed: 0,
|
|
44
|
+
compacted: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Build summary of what was compacted
|
|
48
|
+
const agents = new Set();
|
|
49
|
+
const types = new Map();
|
|
50
|
+
for (const e of remove) {
|
|
51
|
+
agents.add(e.from);
|
|
52
|
+
types.set(e.type, (types.get(e.type) ?? 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
const summaryPayload = {
|
|
55
|
+
compactedCount: remove.length,
|
|
56
|
+
compactedRange: {
|
|
57
|
+
from: remove[0].seq,
|
|
58
|
+
to: remove[remove.length - 1].seq,
|
|
59
|
+
},
|
|
60
|
+
agents: [...agents],
|
|
61
|
+
typeCounts: Object.fromEntries(types),
|
|
62
|
+
compactedAt: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
const logPath = join(projectRoot, COORD_DIR, COORD_LOG);
|
|
65
|
+
// Archival is the durability boundary: never rewrite if it fails.
|
|
66
|
+
const archivePath = `${logPath}.${Date.now()}.archive`;
|
|
67
|
+
const raw = await readFile(logPath, "utf-8");
|
|
68
|
+
await writeFile(archivePath, raw, {
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
flag: "wx",
|
|
71
|
+
mode: 0o600,
|
|
72
|
+
});
|
|
73
|
+
// Write compacted log: summary event + retained events
|
|
74
|
+
const summaryLine = JSON.stringify({
|
|
75
|
+
id: `compaction-${Date.now()}`,
|
|
76
|
+
seq: remove[remove.length - 1].seq,
|
|
77
|
+
type: "status",
|
|
78
|
+
from: "loadout",
|
|
79
|
+
to: "*",
|
|
80
|
+
description: `Compacted ${remove.length} events (seq ${summaryPayload.compactedRange.from}–${summaryPayload.compactedRange.to})`,
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
payload: summaryPayload,
|
|
83
|
+
});
|
|
84
|
+
const lines = [
|
|
85
|
+
summaryLine,
|
|
86
|
+
...finalRetained.map((e) => JSON.stringify(e)),
|
|
87
|
+
].join("\n");
|
|
88
|
+
// Atomic write: write to temp, rename over
|
|
89
|
+
const tmpPath = `${logPath}.tmp`;
|
|
90
|
+
await writeFile(tmpPath, lines + "\n", {
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
mode: 0o600,
|
|
93
|
+
});
|
|
94
|
+
await rename(tmpPath, logPath);
|
|
95
|
+
// Log corrupt lines if any were found
|
|
96
|
+
if (log.corrupt.length > 0) {
|
|
97
|
+
const corruptPath = `${logPath}.${Date.now()}.corrupt`;
|
|
98
|
+
const corruptData = log.corrupt.map((c) => JSON.stringify(c)).join("\n");
|
|
99
|
+
await writeFile(corruptPath, corruptData + "\n", {
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
mode: 0o600,
|
|
102
|
+
}).catch(() => { });
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
before: log.events.length,
|
|
106
|
+
after: finalRetained.length + 1, // +1 for summary
|
|
107
|
+
removed: remove.length,
|
|
108
|
+
compacted: true,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export async function logSize(projectRoot) {
|
|
113
|
+
const logPath = join(projectRoot, COORD_DIR, COORD_LOG);
|
|
114
|
+
try {
|
|
115
|
+
const s = await stat(logPath);
|
|
116
|
+
const log = await readCoordLog(projectRoot);
|
|
117
|
+
return { events: log.events.length, bytes: s.size };
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (typeof error === "object" &&
|
|
121
|
+
error !== null &&
|
|
122
|
+
"code" in error &&
|
|
123
|
+
error.code === "ENOENT") {
|
|
124
|
+
return { events: 0, bytes: 0 };
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Composition boundary for optional provider-driven coordination. */
|
|
2
|
+
import { Codex } from "@openai/codex-sdk";
|
|
3
|
+
import { ClaudeCodeAdapter } from "./adapters/claude-code.js";
|
|
4
|
+
import { CodexAdapter } from "./adapters/codex.js";
|
|
5
|
+
/**
|
|
6
|
+
* Build adapters backed by the provider-supported interfaces shipped with
|
|
7
|
+
* Loadout: Claude Code print-mode CLI and the OpenAI Codex SDK.
|
|
8
|
+
*/
|
|
9
|
+
export function createProviderAdapters() {
|
|
10
|
+
const codex = new Codex();
|
|
11
|
+
const codexDriver = {
|
|
12
|
+
runtimeVersion: "Codex SDK (bundled)",
|
|
13
|
+
startThread(options) {
|
|
14
|
+
return codex.startThread(options);
|
|
15
|
+
},
|
|
16
|
+
resumeThread(sessionId, options) {
|
|
17
|
+
return codex.resumeThread(sessionId, options);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
return [new ClaudeCodeAdapter(), new CodexAdapter(codexDriver)];
|
|
21
|
+
}
|