swarmdo 1.19.0 → 1.29.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/.claude/helpers/auto-memory-hook.mjs +5 -1
- package/.claude-plugin/README.md +3 -3
- package/.claude-plugin/docs/INSTALLATION.md +1 -1
- package/.claude-plugin/docs/PLUGIN_SUMMARY.md +1 -1
- package/.claude-plugin/docs/QUICKSTART.md +3 -3
- package/.claude-plugin/hooks/hooks.json +19 -1
- package/.claude-plugin/marketplace.json +17 -17
- package/.claude-plugin/plugin.json +7 -39
- package/.claude-plugin/scripts/install.sh +3 -3
- package/.claude-plugin/scripts/swarmdo-hook.sh +1 -1
- package/.claude-plugin/scripts/verify.sh +2 -2
- package/README.md +10 -8
- package/package.json +1 -1
- package/v3/@swarmdo/cli/.claude/helpers/auto-memory-hook.mjs +5 -1
- package/v3/@swarmdo/cli/bin/cli.js +38 -0
- package/v3/@swarmdo/cli/dist/src/apply/apply.d.ts +8 -0
- package/v3/@swarmdo/cli/dist/src/apply/apply.js +70 -10
- package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +26 -3
- package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.d.ts +26 -7
- package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.js +97 -13
- package/v3/@swarmdo/cli/dist/src/codegraph/store.d.ts +8 -1
- package/v3/@swarmdo/cli/dist/src/codegraph/store.js +78 -1
- package/v3/@swarmdo/cli/dist/src/commands/apply.js +17 -4
- package/v3/@swarmdo/cli/dist/src/commands/compact-snapshot.d.ts +17 -0
- package/v3/@swarmdo/cli/dist/src/commands/compact-snapshot.js +133 -0
- package/v3/@swarmdo/cli/dist/src/commands/cycles.js +4 -1
- package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +5 -2
- package/v3/@swarmdo/cli/dist/src/commands/index.js +4 -3
- package/v3/@swarmdo/cli/dist/src/commands/pack.d.ts +8 -0
- package/v3/@swarmdo/cli/dist/src/commands/pack.js +28 -2
- package/v3/@swarmdo/cli/dist/src/commands/release.js +5 -1
- package/v3/@swarmdo/cli/dist/src/commands/testreport.js +2 -1
- package/v3/@swarmdo/cli/dist/src/compact/compact.js +17 -7
- package/v3/@swarmdo/cli/dist/src/compact-snapshot/compact-snapshot.d.ts +58 -0
- package/v3/@swarmdo/cli/dist/src/compact-snapshot/compact-snapshot.js +104 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +16 -2
- package/v3/@swarmdo/cli/dist/src/cycles/cycles.d.ts +12 -2
- package/v3/@swarmdo/cli/dist/src/cycles/cycles.js +5 -3
- package/v3/@swarmdo/cli/dist/src/env/env.d.ts +5 -2
- package/v3/@swarmdo/cli/dist/src/env/env.js +24 -3
- package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.d.ts +15 -0
- package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.js +33 -1
- package/v3/@swarmdo/cli/dist/src/init/helpers-generator.js +33 -3
- package/v3/@swarmdo/cli/dist/src/license/license.d.ts +31 -2
- package/v3/@swarmdo/cli/dist/src/license/license.js +133 -11
- package/v3/@swarmdo/cli/dist/src/pack/pack.d.ts +2 -3
- package/v3/@swarmdo/cli/dist/src/pack/pack.js +68 -12
- package/v3/@swarmdo/cli/dist/src/redact/redact.js +14 -2
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.d.ts +16 -0
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.js +33 -3
- package/v3/@swarmdo/cli/dist/src/swarmvector/model-prices.js +12 -2
- package/v3/@swarmdo/cli/dist/src/testreport/testreport.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/testreport/testreport.js +82 -9
- package/v3/@swarmdo/cli/dist/src/transcript/export.d.ts +5 -0
- package/v3/@swarmdo/cli/dist/src/transcript/export.js +8 -1
- package/v3/@swarmdo/cli/dist/src/usage/claude-pricing.d.ts +12 -2
- package/v3/@swarmdo/cli/dist/src/usage/claude-pricing.js +45 -13
- package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.js +5 -2
- package/v3/@swarmdo/cli/package.json +2 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo compact-snapshot` — capture / restore a compaction-survival digest.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo compact-snapshot write # snapshot working state (call on PreCompact)
|
|
5
|
+
* swarmdo compact-snapshot read # print the digest, then consume it (first
|
|
6
|
+
* # prompt after compaction re-injects it)
|
|
7
|
+
* swarmdo compact-snapshot read --keep # print without consuming
|
|
8
|
+
*
|
|
9
|
+
* The digest (recently edited files, uncommitted changes, branch) lets an agent
|
|
10
|
+
* re-ground after context compaction instead of re-exploring. The engine is
|
|
11
|
+
* pure + tested (../compact-snapshot/compact-snapshot.ts); this wrapper does the
|
|
12
|
+
* fs read (edit ledger), git calls, and one-shot persistence.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { output } from '../output.js';
|
|
18
|
+
import { buildDigest, formatDigest, isDigestEmpty, } from '../compact-snapshot/compact-snapshot.js';
|
|
19
|
+
const DIGEST_FILE = 'compact-digest.json';
|
|
20
|
+
const LEDGER_FILE = 'pending-insights.jsonl';
|
|
21
|
+
function dataDir(root) {
|
|
22
|
+
return path.join(root, '.swarmdo', 'data');
|
|
23
|
+
}
|
|
24
|
+
/** Best-effort git; returns '' on any failure (not-a-repo, no git). */
|
|
25
|
+
function git(root, args) {
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 8 * 1024 * 1024 });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Read edit records from the pending-insights ledger. Tolerant of junk lines. */
|
|
34
|
+
function readEdits(root) {
|
|
35
|
+
const p = path.join(dataDir(root), LEDGER_FILE);
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = fs.readFileSync(p, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const line of raw.split('\n')) {
|
|
45
|
+
if (!line.trim())
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const rec = JSON.parse(line);
|
|
49
|
+
if (rec && rec.type === 'edit' && typeof rec.file === 'string') {
|
|
50
|
+
out.push({ file: rec.file, timestamp: typeof rec.timestamp === 'number' ? rec.timestamp : 0 });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* skip malformed line */ }
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function doWrite(root) {
|
|
58
|
+
// Drop swarmdo's own state dirs — they're not the user's working set and
|
|
59
|
+
// are usually gitignored anyway; filtering keeps the digest signal clean.
|
|
60
|
+
const gitStatus = git(root, ['status', '--porcelain'])
|
|
61
|
+
.split('\n')
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.filter((l) => !/(^|\s|"|-> )\.(swarmdo|swarm|git)\//.test(l));
|
|
64
|
+
const digest = buildDigest({
|
|
65
|
+
now: Date.now(),
|
|
66
|
+
edits: readEdits(root),
|
|
67
|
+
gitStatus,
|
|
68
|
+
branch: git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).trim() || undefined,
|
|
69
|
+
});
|
|
70
|
+
if (isDigestEmpty(digest)) {
|
|
71
|
+
// Nothing worth restoring — remove any stale digest so a later `read` is clean.
|
|
72
|
+
try {
|
|
73
|
+
fs.rmSync(path.join(dataDir(root), DIGEST_FILE));
|
|
74
|
+
}
|
|
75
|
+
catch { /* none */ }
|
|
76
|
+
output.writeln('compact-snapshot: no working state to capture');
|
|
77
|
+
return { success: true, exitCode: 0 };
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
fs.mkdirSync(dataDir(root), { recursive: true });
|
|
81
|
+
fs.writeFileSync(path.join(dataDir(root), DIGEST_FILE), JSON.stringify(digest, null, 2) + '\n');
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
output.printError(`compact-snapshot: could not write digest — ${e.message}`);
|
|
85
|
+
return { success: false, exitCode: 1 };
|
|
86
|
+
}
|
|
87
|
+
output.writeln(`compact-snapshot: captured ${digest.recentFiles.length} edited + ${digest.uncommitted.length} uncommitted`);
|
|
88
|
+
return { success: true, exitCode: 0 };
|
|
89
|
+
}
|
|
90
|
+
function doRead(root, keep) {
|
|
91
|
+
const file = path.join(dataDir(root), DIGEST_FILE);
|
|
92
|
+
let digest;
|
|
93
|
+
try {
|
|
94
|
+
digest = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { success: true, exitCode: 0 };
|
|
98
|
+
} // no digest → nothing to inject
|
|
99
|
+
const text = formatDigest(digest, Date.now());
|
|
100
|
+
if (text)
|
|
101
|
+
process.stdout.write(text + '\n');
|
|
102
|
+
if (!keep) {
|
|
103
|
+
try {
|
|
104
|
+
fs.rmSync(file);
|
|
105
|
+
}
|
|
106
|
+
catch { /* already gone */ }
|
|
107
|
+
} // consume-once
|
|
108
|
+
return { success: true, exitCode: 0 };
|
|
109
|
+
}
|
|
110
|
+
async function run(ctx) {
|
|
111
|
+
const root = ctx.cwd || process.cwd();
|
|
112
|
+
const mode = (ctx.args[0] ?? 'read').toLowerCase();
|
|
113
|
+
if (mode === 'write')
|
|
114
|
+
return doWrite(root);
|
|
115
|
+
if (mode === 'read')
|
|
116
|
+
return doRead(root, ctx.flags.keep === true);
|
|
117
|
+
output.printError(`compact-snapshot: unknown mode "${mode}" (use write | read)`);
|
|
118
|
+
return { success: false, exitCode: 1 };
|
|
119
|
+
}
|
|
120
|
+
export const compactSnapshotCommand = {
|
|
121
|
+
name: 'compact-snapshot',
|
|
122
|
+
description: 'Capture/restore a working-state digest that survives context compaction — recent edits, uncommitted changes, branch — so an agent re-grounds instead of re-exploring',
|
|
123
|
+
options: [
|
|
124
|
+
{ name: 'keep', description: 'on read, print the digest without consuming it', type: 'boolean' },
|
|
125
|
+
],
|
|
126
|
+
examples: [
|
|
127
|
+
{ command: 'swarmdo compact-snapshot write', description: 'Snapshot working state (wire to a PreCompact hook)' },
|
|
128
|
+
{ command: 'swarmdo compact-snapshot read', description: 'Print + consume the digest (wire to the first post-compaction prompt)' },
|
|
129
|
+
],
|
|
130
|
+
action: run,
|
|
131
|
+
};
|
|
132
|
+
export default compactSnapshotCommand;
|
|
133
|
+
//# sourceMappingURL=compact-snapshot.js.map
|
|
@@ -15,6 +15,7 @@ async function run(ctx) {
|
|
|
15
15
|
const root = ctx.cwd || process.cwd();
|
|
16
16
|
const asJson = ctx.flags.format === 'json';
|
|
17
17
|
const ci = ctx.flags.ci === true;
|
|
18
|
+
const includeTypeOnly = ctx.flags['include-type-only'] === true;
|
|
18
19
|
let index = loadIndex(root);
|
|
19
20
|
if (!index) {
|
|
20
21
|
index = scanRepo(root);
|
|
@@ -23,7 +24,7 @@ async function run(ctx) {
|
|
|
23
24
|
}
|
|
24
25
|
catch { /* read-only fs — index is in memory */ }
|
|
25
26
|
}
|
|
26
|
-
const res = findCycles(index);
|
|
27
|
+
const res = findCycles(index, { includeTypeOnly });
|
|
27
28
|
const count = res.cycles.length + res.selfLoops.length;
|
|
28
29
|
if (asJson) {
|
|
29
30
|
process.stdout.write(JSON.stringify({ count, ...res }, null, 2) + '\n');
|
|
@@ -40,10 +41,12 @@ export const cyclesCommand = {
|
|
|
40
41
|
description: 'Find circular import dependencies via the import graph (madge --circular style) — catch the TDZ/undefined-export bugs they cause',
|
|
41
42
|
options: [
|
|
42
43
|
{ name: 'ci', description: 'exit 1 if any circular dependency exists (gate a build)', type: 'boolean' },
|
|
44
|
+
{ name: 'include-type-only', description: 'also count TypeScript `import type` edges (excluded by default — they erase at compile time and cause no runtime cycle)', type: 'boolean' },
|
|
43
45
|
],
|
|
44
46
|
examples: [
|
|
45
47
|
{ command: 'swarmdo cycles', description: 'List circular imports in the repo' },
|
|
46
48
|
{ command: 'swarmdo cycles --ci', description: 'Fail CI when a cycle is introduced' },
|
|
49
|
+
{ command: 'swarmdo cycles --include-type-only', description: 'Strict view: count type-only imports too' },
|
|
47
50
|
],
|
|
48
51
|
action: run,
|
|
49
52
|
};
|
|
@@ -33,8 +33,11 @@ async function run(ctx) {
|
|
|
33
33
|
}
|
|
34
34
|
// Global --format (text|json|table); text and table both render the table.
|
|
35
35
|
const asJson = ctx.flags.format === 'json';
|
|
36
|
-
// Capture history: SOH-delimited header + numstat, no merges.
|
|
37
|
-
|
|
36
|
+
// Capture history: SOH-delimited header + numstat, no merges. `%aN` (not
|
|
37
|
+
// `%an`) resolves author names through `.mailmap`, so name/email variants of
|
|
38
|
+
// the same person fold into one identity — otherwise the author-spread factor
|
|
39
|
+
// in the risk score is silently inflated. Identical to `%an` when no .mailmap.
|
|
40
|
+
const args = ['log', '--no-merges', '--numstat', `--since=${since}`, '--format=format:%x01%H%x1f%aN%x1f%aI'];
|
|
38
41
|
if (pathArg)
|
|
39
42
|
args.push('--', pathArg);
|
|
40
43
|
let raw;
|
|
@@ -22,6 +22,7 @@ const commandLoaders = {
|
|
|
22
22
|
// Deterministic command-output compression (rtk/headroom demand) — a
|
|
23
23
|
// zero-token stream filter, distinct from caveman `compress` for files.
|
|
24
24
|
compact: () => import('./compact.js'),
|
|
25
|
+
'compact-snapshot': () => import('./compact-snapshot.js'),
|
|
25
26
|
// Secret detection + redaction on the agent data path (gitleaks/trufflehog
|
|
26
27
|
// demand) — mask API keys/tokens before they reach an LLM/log/memory.
|
|
27
28
|
redact: () => import('./redact.js'),
|
|
@@ -286,7 +287,7 @@ export async function getCommandsByCategory() {
|
|
|
286
287
|
// three slots from 'statusline' onward (completionsCmd received the
|
|
287
288
|
// statusline module, analyzeCmd received completions, …), scrambling the
|
|
288
289
|
// categorized help. Every load now has a named slot.
|
|
289
|
-
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, testreportCmd,] = await Promise.all([
|
|
290
|
+
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, testreportCmd, compactSnapshotCmd,] = await Promise.all([
|
|
290
291
|
loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
|
|
291
292
|
loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
|
|
292
293
|
loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
|
|
@@ -294,7 +295,7 @@ export async function getCommandsByCategory() {
|
|
|
294
295
|
loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
|
|
295
296
|
loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
|
|
296
297
|
loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
|
|
297
|
-
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'), loadCommand('hotspots'), loadCommand('affected'), loadCommand('cycles'), loadCommand('testreport'),
|
|
298
|
+
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'), loadCommand('hotspots'), loadCommand('affected'), loadCommand('cycles'), loadCommand('testreport'), loadCommand('compact-snapshot'),
|
|
298
299
|
]);
|
|
299
300
|
return {
|
|
300
301
|
primary: [
|
|
@@ -310,7 +311,7 @@ export async function getCommandsByCategory() {
|
|
|
310
311
|
utility: [
|
|
311
312
|
configCmd, doctorCmd, daemonCmd, completionsCmd,
|
|
312
313
|
migrateCmd, workflowCmd, demoCmd,
|
|
313
|
-
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, testreportCmd, efficiencyCmd,
|
|
314
|
+
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, testreportCmd, compactSnapshotCmd, efficiencyCmd,
|
|
314
315
|
].filter(Boolean),
|
|
315
316
|
analysis: [
|
|
316
317
|
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
* land in the bundle.
|
|
14
14
|
*/
|
|
15
15
|
import type { Command } from '../types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Decode a file buffer to bundle text, honoring BOMs (#10). UTF-16 LE/BE
|
|
18
|
+
* files decode to text (previously their NUL bytes tripped the binary
|
|
19
|
+
* heuristic and the file was silently dropped); a UTF-8 BOM is stripped
|
|
20
|
+
* instead of leaking into the bundle. Returns null for genuinely-binary
|
|
21
|
+
* content (NUL byte without a text BOM). Exported for tests.
|
|
22
|
+
*/
|
|
23
|
+
export declare function decodeText(buf: Buffer): string | null;
|
|
16
24
|
export declare const packCommand: Command;
|
|
17
25
|
export default packCommand;
|
|
18
26
|
//# sourceMappingURL=pack.d.ts.map
|
|
@@ -35,6 +35,31 @@ function looksBinary(buf) {
|
|
|
35
35
|
return true;
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Decode a file buffer to bundle text, honoring BOMs (#10). UTF-16 LE/BE
|
|
40
|
+
* files decode to text (previously their NUL bytes tripped the binary
|
|
41
|
+
* heuristic and the file was silently dropped); a UTF-8 BOM is stripped
|
|
42
|
+
* instead of leaking into the bundle. Returns null for genuinely-binary
|
|
43
|
+
* content (NUL byte without a text BOM). Exported for tests.
|
|
44
|
+
*/
|
|
45
|
+
export function decodeText(buf) {
|
|
46
|
+
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
|
|
47
|
+
return buf.subarray(3).toString('utf8');
|
|
48
|
+
}
|
|
49
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
|
50
|
+
const body = buf.subarray(2);
|
|
51
|
+
if (body.length % 2 !== 0)
|
|
52
|
+
return null;
|
|
53
|
+
return body.toString('utf16le');
|
|
54
|
+
}
|
|
55
|
+
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
|
56
|
+
const body = Buffer.from(buf.subarray(2)); // copy: swap16 mutates in place
|
|
57
|
+
if (body.length % 2 !== 0)
|
|
58
|
+
return null;
|
|
59
|
+
return body.swap16().toString('utf16le');
|
|
60
|
+
}
|
|
61
|
+
return looksBinary(buf) ? null : buf.toString('utf8');
|
|
62
|
+
}
|
|
38
63
|
function walk(o) {
|
|
39
64
|
const files = [];
|
|
40
65
|
const stack = [o.root];
|
|
@@ -82,9 +107,10 @@ function walk(o) {
|
|
|
82
107
|
catch {
|
|
83
108
|
continue;
|
|
84
109
|
}
|
|
85
|
-
|
|
110
|
+
const text = decodeText(buf);
|
|
111
|
+
if (text === null)
|
|
86
112
|
continue;
|
|
87
|
-
files.push({ path: rel, content:
|
|
113
|
+
files.push({ path: rel, content: text });
|
|
88
114
|
}
|
|
89
115
|
}
|
|
90
116
|
}
|
|
@@ -15,6 +15,10 @@ import * as os from 'node:os';
|
|
|
15
15
|
import { execFileSync } from 'node:child_process';
|
|
16
16
|
import { output } from '../output.js';
|
|
17
17
|
import { planRelease, renderStep } from '../release/release.js';
|
|
18
|
+
/** Escape every regex metacharacter (incl. backslash) so a value is a literal in `new RegExp`. */
|
|
19
|
+
function escapeRegExp(s) {
|
|
20
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
|
+
}
|
|
18
22
|
function repoRootFrom(cwd) {
|
|
19
23
|
try {
|
|
20
24
|
return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
@@ -47,7 +51,7 @@ function syncDocs(root, files, current, next) {
|
|
|
47
51
|
let after = before;
|
|
48
52
|
// exact patterns used by every release since 1.4.x
|
|
49
53
|
after = after.replaceAll(`**Swarmdo v${current}** (`, `**Swarmdo v${next}** (`);
|
|
50
|
-
after = after.replace(new RegExp(`\\*\\*Swarmdo v${next
|
|
54
|
+
after = after.replace(new RegExp(`\\*\\*Swarmdo v${escapeRegExp(next)}\\*\\* \\(\\d{4}-\\d{2}-\\d{2}\\)`), `**Swarmdo v${next}** (${today})`);
|
|
51
55
|
after = after.replaceAll(`swarmdo@${current}\` (umbrella), \`@swarmdo/cli@${current}\`, \`swarmdo-bridge@${current}\``, `swarmdo@${next}\` (umbrella), \`@swarmdo/cli@${next}\`, \`swarmdo-bridge@${next}\``);
|
|
52
56
|
// README badge + website carry the last PUBLISHED version, which lags
|
|
53
57
|
// the repo version (`current`) between releases — match any semver in
|
|
@@ -98,7 +98,8 @@ async function run(ctx) {
|
|
|
98
98
|
else {
|
|
99
99
|
output.writeln(formatSummary(summary, { top: top > 0 ? top : undefined }));
|
|
100
100
|
}
|
|
101
|
-
|
|
101
|
+
// A bail-out aborts the suite → results are incomplete → treat as failure in CI.
|
|
102
|
+
const code = ci && (summary.failed > 0 || summary.bailedOut) ? 1 : 0;
|
|
102
103
|
return { success: code === 0, exitCode: code };
|
|
103
104
|
}
|
|
104
105
|
export const testreportCommand = {
|
|
@@ -95,21 +95,31 @@ function foldNodeFrames(lines) {
|
|
|
95
95
|
}
|
|
96
96
|
return out;
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Collapse runs of ≥3 consecutive blank lines to a single blank. Runs of 1 or 2
|
|
100
|
+
* blanks are preserved (a 2-blank run is meaningful — e.g. PEP 8 spacing between
|
|
101
|
+
* top-level defs), so the full run length is counted before deciding.
|
|
102
|
+
*/
|
|
99
103
|
function collapseBlankLines(lines) {
|
|
100
104
|
const out = [];
|
|
101
105
|
let blanks = 0;
|
|
106
|
+
const flush = () => {
|
|
107
|
+
if (blanks === 0)
|
|
108
|
+
return;
|
|
109
|
+
const keep = blanks >= 3 ? 1 : blanks; // ≥3 → 1; 1 or 2 → unchanged
|
|
110
|
+
for (let i = 0; i < keep; i++)
|
|
111
|
+
out.push('');
|
|
112
|
+
blanks = 0;
|
|
113
|
+
};
|
|
102
114
|
for (const line of lines) {
|
|
103
115
|
if (line.trim() === '') {
|
|
104
116
|
blanks++;
|
|
105
|
-
|
|
106
|
-
out.push('');
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
blanks = 0;
|
|
110
|
-
out.push(line);
|
|
117
|
+
continue;
|
|
111
118
|
}
|
|
119
|
+
flush();
|
|
120
|
+
out.push(line);
|
|
112
121
|
}
|
|
122
|
+
flush();
|
|
113
123
|
return out;
|
|
114
124
|
}
|
|
115
125
|
/** Keep head + tail lines, eliding the middle with a marker. */
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compact-snapshot.ts — build a "survival digest" that lets an agent re-ground
|
|
3
|
+
* after context compaction. Post-compaction, the model loses its working set
|
|
4
|
+
* and tends to re-explore files it just edited and repeat corrected mistakes
|
|
5
|
+
* (a widely-reported pain, #45). This captures the cheap signals swarmdo
|
|
6
|
+
* already tracks — recently edited files (the pending-insights edit ledger),
|
|
7
|
+
* uncommitted changes (`git status --porcelain`), and the current branch —
|
|
8
|
+
* into a compact digest that the first post-compaction prompt re-injects.
|
|
9
|
+
*
|
|
10
|
+
* Pure + deterministic: inputs in (including `now`, so no clock dependency),
|
|
11
|
+
* a digest / rendered string out. The fs read (edit ledger), git calls, and
|
|
12
|
+
* digest persistence live in ../commands/compact-snapshot.ts, so this is
|
|
13
|
+
* fixture-testable.
|
|
14
|
+
*/
|
|
15
|
+
/** One edit-ledger record (a line of .swarmdo/data/pending-insights.jsonl). */
|
|
16
|
+
export interface EditRecord {
|
|
17
|
+
file: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}
|
|
20
|
+
export interface DigestInput {
|
|
21
|
+
/** recent edit-ledger records; most-recent-wins when a file repeats */
|
|
22
|
+
edits?: EditRecord[];
|
|
23
|
+
/** raw `git status --porcelain` lines (uncommitted changes) */
|
|
24
|
+
gitStatus?: string[];
|
|
25
|
+
/** current branch name, if known */
|
|
26
|
+
branch?: string;
|
|
27
|
+
/** epoch ms the snapshot was taken (injected, not read from a clock) */
|
|
28
|
+
now: number;
|
|
29
|
+
}
|
|
30
|
+
export interface CompactDigest {
|
|
31
|
+
takenAt: number;
|
|
32
|
+
branch?: string;
|
|
33
|
+
/** distinct edited files, most-recently-edited first, capped */
|
|
34
|
+
recentFiles: string[];
|
|
35
|
+
/** distinct uncommitted file paths, capped */
|
|
36
|
+
uncommitted: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract the working-tree path from one `git status --porcelain` line.
|
|
40
|
+
* Format is `XY <path>` (two status chars + space); a rename is
|
|
41
|
+
* `R <old> -> <new>` — we want the destination. Quoted paths (git quotes
|
|
42
|
+
* names with special chars) are unwrapped. Returns null for a blank line.
|
|
43
|
+
* Pure.
|
|
44
|
+
*/
|
|
45
|
+
export declare function porcelainPath(line: string): string | null;
|
|
46
|
+
/** Build the digest from raw session signals. Pure. */
|
|
47
|
+
export declare function buildDigest(input: DigestInput, opts?: {
|
|
48
|
+
maxFiles?: number;
|
|
49
|
+
}): CompactDigest;
|
|
50
|
+
/** A digest with no files and no branch carries nothing worth re-injecting. Pure. */
|
|
51
|
+
export declare function isDigestEmpty(d: CompactDigest): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Render the digest as a re-grounding block for the first post-compaction
|
|
54
|
+
* prompt. Empty digest → '' (nothing to inject). `now` is passed so the
|
|
55
|
+
* relative age is deterministic. Pure.
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatDigest(d: CompactDigest, now: number): string;
|
|
58
|
+
//# sourceMappingURL=compact-snapshot.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compact-snapshot.ts — build a "survival digest" that lets an agent re-ground
|
|
3
|
+
* after context compaction. Post-compaction, the model loses its working set
|
|
4
|
+
* and tends to re-explore files it just edited and repeat corrected mistakes
|
|
5
|
+
* (a widely-reported pain, #45). This captures the cheap signals swarmdo
|
|
6
|
+
* already tracks — recently edited files (the pending-insights edit ledger),
|
|
7
|
+
* uncommitted changes (`git status --porcelain`), and the current branch —
|
|
8
|
+
* into a compact digest that the first post-compaction prompt re-injects.
|
|
9
|
+
*
|
|
10
|
+
* Pure + deterministic: inputs in (including `now`, so no clock dependency),
|
|
11
|
+
* a digest / rendered string out. The fs read (edit ledger), git calls, and
|
|
12
|
+
* digest persistence live in ../commands/compact-snapshot.ts, so this is
|
|
13
|
+
* fixture-testable.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_MAX_FILES = 12;
|
|
16
|
+
/**
|
|
17
|
+
* Extract the working-tree path from one `git status --porcelain` line.
|
|
18
|
+
* Format is `XY <path>` (two status chars + space); a rename is
|
|
19
|
+
* `R <old> -> <new>` — we want the destination. Quoted paths (git quotes
|
|
20
|
+
* names with special chars) are unwrapped. Returns null for a blank line.
|
|
21
|
+
* Pure.
|
|
22
|
+
*/
|
|
23
|
+
export function porcelainPath(line) {
|
|
24
|
+
if (!line || line.length < 4)
|
|
25
|
+
return null;
|
|
26
|
+
let rest = line.slice(3); // strip the 2 status chars + the separating space
|
|
27
|
+
const arrow = rest.indexOf(' -> ');
|
|
28
|
+
if (arrow >= 0)
|
|
29
|
+
rest = rest.slice(arrow + 4); // rename/copy → destination path
|
|
30
|
+
rest = rest.trim();
|
|
31
|
+
if (rest.startsWith('"') && rest.endsWith('"') && rest.length >= 2) {
|
|
32
|
+
rest = rest.slice(1, -1); // unwrap git's quoted path (escapes left as-is)
|
|
33
|
+
}
|
|
34
|
+
return rest || null;
|
|
35
|
+
}
|
|
36
|
+
/** Build the digest from raw session signals. Pure. */
|
|
37
|
+
export function buildDigest(input, opts = {}) {
|
|
38
|
+
const max = opts.maxFiles ?? DEFAULT_MAX_FILES;
|
|
39
|
+
// Distinct edited files, most-recent edit first. A file edited repeatedly
|
|
40
|
+
// keeps its latest timestamp; ties preserve first-seen (stable) order.
|
|
41
|
+
const latest = new Map();
|
|
42
|
+
for (const e of input.edits ?? []) {
|
|
43
|
+
if (!e || typeof e.file !== 'string' || !e.file)
|
|
44
|
+
continue;
|
|
45
|
+
const t = typeof e.timestamp === 'number' ? e.timestamp : 0;
|
|
46
|
+
const prev = latest.get(e.file);
|
|
47
|
+
if (prev === undefined || t >= prev)
|
|
48
|
+
latest.set(e.file, t);
|
|
49
|
+
}
|
|
50
|
+
const recentFiles = [...latest.entries()]
|
|
51
|
+
.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
52
|
+
.slice(0, max)
|
|
53
|
+
.map(([f]) => f);
|
|
54
|
+
const seenUncommitted = new Set();
|
|
55
|
+
const uncommitted = [];
|
|
56
|
+
for (const line of input.gitStatus ?? []) {
|
|
57
|
+
const p = porcelainPath(line);
|
|
58
|
+
if (p && !seenUncommitted.has(p)) {
|
|
59
|
+
seenUncommitted.add(p);
|
|
60
|
+
uncommitted.push(p);
|
|
61
|
+
}
|
|
62
|
+
if (uncommitted.length >= max)
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
takenAt: input.now,
|
|
67
|
+
...(input.branch ? { branch: input.branch } : {}),
|
|
68
|
+
recentFiles,
|
|
69
|
+
uncommitted,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** A digest with no files and no branch carries nothing worth re-injecting. Pure. */
|
|
73
|
+
export function isDigestEmpty(d) {
|
|
74
|
+
return d.recentFiles.length === 0 && d.uncommitted.length === 0;
|
|
75
|
+
}
|
|
76
|
+
/** Compact human "Nm ago" for the snapshot age. Pure. */
|
|
77
|
+
function ago(fromMs, now) {
|
|
78
|
+
const s = Math.max(0, Math.round((now - fromMs) / 1000));
|
|
79
|
+
if (s < 60)
|
|
80
|
+
return `${s}s ago`;
|
|
81
|
+
const m = Math.round(s / 60);
|
|
82
|
+
if (m < 60)
|
|
83
|
+
return `${m}m ago`;
|
|
84
|
+
return `${Math.round(m / 60)}h ago`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Render the digest as a re-grounding block for the first post-compaction
|
|
88
|
+
* prompt. Empty digest → '' (nothing to inject). `now` is passed so the
|
|
89
|
+
* relative age is deterministic. Pure.
|
|
90
|
+
*/
|
|
91
|
+
export function formatDigest(d, now) {
|
|
92
|
+
if (isDigestEmpty(d))
|
|
93
|
+
return '';
|
|
94
|
+
const lines = [`[swarmdo] Working context restored after compaction (snapshot ${ago(d.takenAt, now)}):`];
|
|
95
|
+
if (d.branch)
|
|
96
|
+
lines.push(`- Branch: ${d.branch}`);
|
|
97
|
+
if (d.recentFiles.length)
|
|
98
|
+
lines.push(`- Recently edited (${d.recentFiles.length}): ${d.recentFiles.join(', ')}`);
|
|
99
|
+
if (d.uncommitted.length)
|
|
100
|
+
lines.push(`- Uncommitted changes (${d.uncommitted.length}): ${d.uncommitted.join(', ')}`);
|
|
101
|
+
lines.push('Resume this work; re-read these files only as needed rather than re-exploring from scratch.');
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=compact-snapshot.js.map
|
|
@@ -13,9 +13,23 @@ export const TOPOLOGIES = ['hierarchical', 'mesh', 'hierarchical-mesh', 'ring',
|
|
|
13
13
|
export const MEMORY_BACKENDS = ['agentdb', 'sqlite', 'hybrid', 'memory'];
|
|
14
14
|
import { parseOpenRouterConfig } from '../providers/openrouter-config.js';
|
|
15
15
|
export const KNOWN_CONFIG_KEYS = ['topology', 'maxAgents', 'strategy', 'consensus', 'memory', 'memoryBackend', 'hnsw', 'neural', 'embeddings', 'providers', 'mcp', 'logging', 'daemon', 'hooks', 'version', 'openrouter', '$schema'];
|
|
16
|
+
// Current Claude Code hook events (source: code.claude.com/docs/en/hooks).
|
|
17
|
+
// Kept in sync with the runtime; a stale list here false-warns on valid hooks.
|
|
16
18
|
export const HOOK_EVENTS = [
|
|
17
|
-
|
|
18
|
-
'SessionStart', '
|
|
19
|
+
// session lifecycle
|
|
20
|
+
'SessionStart', 'Setup', 'SessionEnd',
|
|
21
|
+
// per-turn
|
|
22
|
+
'UserPromptSubmit', 'UserPromptExpansion', 'Stop', 'StopFailure',
|
|
23
|
+
// agentic loop
|
|
24
|
+
'PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'PostToolBatch', 'PermissionRequest', 'PermissionDenied',
|
|
25
|
+
// subagent & task
|
|
26
|
+
'SubagentStart', 'SubagentStop', 'TeammateIdle', 'TaskCreated', 'TaskCompleted',
|
|
27
|
+
// system & file
|
|
28
|
+
'Notification', 'MessageDisplay', 'CwdChanged', 'FileChanged', 'ConfigChange', 'InstructionsLoaded', 'PreCompact', 'PostCompact',
|
|
29
|
+
// MCP elicitation
|
|
30
|
+
'Elicitation', 'ElicitationResult',
|
|
31
|
+
// worktree
|
|
32
|
+
'WorktreeCreate', 'WorktreeRemove',
|
|
19
33
|
];
|
|
20
34
|
/** Parse a JSON file's raw text; a null raw means "file absent" (fine). */
|
|
21
35
|
export function lintJson(file, raw) {
|
|
@@ -12,8 +12,18 @@
|
|
|
12
12
|
* The index load lives in ../commands/cycles.ts, so this is fixture-testable.
|
|
13
13
|
*/
|
|
14
14
|
import type { CodeIndex } from '../codegraph/codegraph.js';
|
|
15
|
+
export interface CycleOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Include TypeScript `import type`/`export type` edges. They erase at compile
|
|
18
|
+
* time so a type-only "cycle" causes none of the runtime bugs this detector
|
|
19
|
+
* exists to catch — excluded by DEFAULT to avoid false positives. Set true for
|
|
20
|
+
* a strict structural view. An edge counts as a runtime edge if ANY import
|
|
21
|
+
* between the two files is a value import (mixed imports keep the edge).
|
|
22
|
+
*/
|
|
23
|
+
includeTypeOnly?: boolean;
|
|
24
|
+
}
|
|
15
25
|
/** Build file → sorted list of internal (resolved) imports. Pure. */
|
|
16
|
-
export declare function buildAdjacency(index: CodeIndex): Map<string, string[]>;
|
|
26
|
+
export declare function buildAdjacency(index: CodeIndex, opts?: CycleOptions): Map<string, string[]>;
|
|
17
27
|
/**
|
|
18
28
|
* Tarjan's SCC. Returns every strongly-connected component as a member list.
|
|
19
29
|
* Iterative (no recursion) so deep graphs don't overflow the stack. Deterministic:
|
|
@@ -31,7 +41,7 @@ export interface CycleResult {
|
|
|
31
41
|
* it has ≥2 files (mutual reachability) or a single file with a self-edge.
|
|
32
42
|
* Deterministic: groups sorted by size desc then first member.
|
|
33
43
|
*/
|
|
34
|
-
export declare function findCycles(index: CodeIndex): CycleResult;
|
|
44
|
+
export declare function findCycles(index: CodeIndex, opts?: CycleOptions): CycleResult;
|
|
35
45
|
/** Human-readable summary. Pure. */
|
|
36
46
|
export declare function formatCycles(res: CycleResult): string;
|
|
37
47
|
//# sourceMappingURL=cycles.d.ts.map
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The index load lives in ../commands/cycles.ts, so this is fixture-testable.
|
|
13
13
|
*/
|
|
14
14
|
/** Build file → sorted list of internal (resolved) imports. Pure. */
|
|
15
|
-
export function buildAdjacency(index) {
|
|
15
|
+
export function buildAdjacency(index, opts = {}) {
|
|
16
16
|
const adj = new Map();
|
|
17
17
|
const ensure = (f) => {
|
|
18
18
|
let l = adj.get(f);
|
|
@@ -25,6 +25,8 @@ export function buildAdjacency(index) {
|
|
|
25
25
|
for (const e of index.imports) {
|
|
26
26
|
if (!e.resolved)
|
|
27
27
|
continue; // external — not part of internal cycles
|
|
28
|
+
if (e.isTypeOnly && !opts.includeTypeOnly)
|
|
29
|
+
continue; // type-only → no runtime edge
|
|
28
30
|
const l = ensure(e.from);
|
|
29
31
|
if (!l.includes(e.resolved))
|
|
30
32
|
l.push(e.resolved);
|
|
@@ -101,8 +103,8 @@ export function stronglyConnectedComponents(adj) {
|
|
|
101
103
|
* it has ≥2 files (mutual reachability) or a single file with a self-edge.
|
|
102
104
|
* Deterministic: groups sorted by size desc then first member.
|
|
103
105
|
*/
|
|
104
|
-
export function findCycles(index) {
|
|
105
|
-
const adj = buildAdjacency(index);
|
|
106
|
+
export function findCycles(index, opts = {}) {
|
|
107
|
+
const adj = buildAdjacency(index, opts);
|
|
106
108
|
const selfLoops = [];
|
|
107
109
|
for (const [from, tos] of adj)
|
|
108
110
|
if (tos.includes(from))
|
|
@@ -29,8 +29,11 @@ export interface EnvReport {
|
|
|
29
29
|
export declare function extractEnvRefs(source: string, file: string): EnvRef[];
|
|
30
30
|
/**
|
|
31
31
|
* Parse a `.env` file into an ordered list of declared keys. Handles `KEY=val`,
|
|
32
|
-
* `export KEY=val`, quoted values, `# comments`, and blank lines.
|
|
33
|
-
*
|
|
32
|
+
* `export KEY=val`, quoted values, `# comments`, and blank lines. The key
|
|
33
|
+
* charset + `:` separator mirror the reference `dotenv` package's grammar
|
|
34
|
+
* (`[\w.-]+` with `=` or `: ` as separator), so dotted/hyphenated keys like
|
|
35
|
+
* `APP.NAME` aren't silently dropped. Values are ignored — we only reconcile
|
|
36
|
+
* key presence. Pure.
|
|
34
37
|
*/
|
|
35
38
|
export declare function parseDotenv(text: string): string[];
|
|
36
39
|
export interface ReconcileInput {
|