@really-knows-ai/foundry 2.3.2 → 3.0.1
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/README.md +180 -369
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +533 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +433 -0
- package/dist/docs/concepts.md +395 -0
- package/dist/docs/getting-started.md +344 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +328 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +62 -40
- package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +191 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -100
- package/docs/concepts.md +0 -122
- package/docs/getting-started.md +0 -187
- package/docs/work-spec.md +0 -207
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -111
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sort routing helpers — pure functions that decide the next stage given
|
|
3
|
+
* the current stage list, history, feedback, and iteration counters.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `src/scripts/sort.js` to keep that file under the
|
|
6
|
+
* configured `max-lines` limit and to lower per-function complexity.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Spec §6.1: an item is "open" (still in flight) when its head state is
|
|
10
|
+
// 'open', 'actioned', 'rejected', or 'wont-fix' — equivalently, when the
|
|
11
|
+
// state is neither 'resolved' nor 'deadlocked'.
|
|
12
|
+
const isOpenItem = (f) => f.state !== 'resolved' && f.state !== 'deadlocked';
|
|
13
|
+
|
|
14
|
+
export function baseStage(stage) {
|
|
15
|
+
return stage.split(':')[0];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function findFirst(stages, base) {
|
|
19
|
+
for (const s of stages) {
|
|
20
|
+
if (baseStage(s) === base) return s;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function nextInRoute(stages, current) {
|
|
26
|
+
const idx = stages.indexOf(current);
|
|
27
|
+
if (idx !== -1 && idx + 1 < stages.length) {
|
|
28
|
+
return stages[idx + 1];
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasItemsNeedingForge(openItems) {
|
|
34
|
+
return openItems.some(f => f.state === 'open' || f.state === 'rejected');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasItemsPendingApproval(openItems) {
|
|
38
|
+
return openItems.some(f => f.state === 'actioned' || f.state === 'wont-fix');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function routeForgeIfNeeded(stages, forgeCount, maxIterations) {
|
|
42
|
+
if (forgeCount >= maxIterations) return 'blocked';
|
|
43
|
+
return findFirst(stages, 'forge') ?? 'blocked';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appraiseForgeOrApproval(stages, openItems, forgeCount, maxIterations) {
|
|
47
|
+
if (hasItemsNeedingForge(openItems)) {
|
|
48
|
+
return routeForgeIfNeeded(stages, forgeCount, maxIterations);
|
|
49
|
+
}
|
|
50
|
+
if (hasItemsPendingApproval(openItems)) {
|
|
51
|
+
return findFirst(stages, 'appraise') ?? 'blocked';
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function nextAfterAppraise({ stages, current, feedback, forgeCount, maxIterations }) {
|
|
57
|
+
// Note: deadlock detection is handled by runDeadlockPass at the top of
|
|
58
|
+
// runSort (spec §6.1). This helper assumes routing has already been allowed
|
|
59
|
+
// to fall through (i.e., no item qualifies as deadlocked).
|
|
60
|
+
const openItems = feedback.filter(isOpenItem);
|
|
61
|
+
const decided = appraiseForgeOrApproval(stages, openItems, forgeCount, maxIterations);
|
|
62
|
+
if (decided !== null) return decided;
|
|
63
|
+
return nextInRoute(stages, current) ?? 'done';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function nextAfterQuench(stages, current, feedback, forgeCount, maxIterations) {
|
|
67
|
+
const openItems = feedback.filter(isOpenItem);
|
|
68
|
+
const needsForge = openItems.some(f => f.state === 'open' || f.state === 'rejected');
|
|
69
|
+
if (needsForge) return routeForgeIfNeeded(stages, forgeCount, maxIterations);
|
|
70
|
+
return nextInRoute(stages, current) ?? 'done';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function lastNonSortStage(history) {
|
|
74
|
+
const nonSort = history.filter(e => baseStage(e.stage || '') !== 'sort');
|
|
75
|
+
if (nonSort.length === 0) return null;
|
|
76
|
+
return nonSort[nonSort.length - 1].stage;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildRouteHandlers({ stages, lastEntry, feedback, forgeCount, maxIterations }) {
|
|
80
|
+
const appraiseRoute = () => nextAfterAppraise({
|
|
81
|
+
stages, current: lastEntry, feedback, forgeCount, maxIterations,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
'assay': () => findFirst(stages, 'forge') ?? 'blocked',
|
|
85
|
+
'forge': () => nextInRoute(stages, lastEntry) ?? 'done',
|
|
86
|
+
'quench': () => nextAfterQuench(stages, lastEntry, feedback, forgeCount, maxIterations),
|
|
87
|
+
'appraise': appraiseRoute,
|
|
88
|
+
'human-appraise': appraiseRoute,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function determineRoute(stages, history, feedback, maxIterations) {
|
|
93
|
+
const forgeCount = history.filter(e => baseStage(e.stage || '') === 'forge').length;
|
|
94
|
+
const lastEntry = lastNonSortStage(history);
|
|
95
|
+
if (lastEntry === null) return stages[0];
|
|
96
|
+
const handlers = buildRouteHandlers({
|
|
97
|
+
stages, lastEntry, feedback, forgeCount, maxIterations,
|
|
98
|
+
});
|
|
99
|
+
const handler = handlers[baseStage(lastEntry)];
|
|
100
|
+
return handler ? handler() : 'blocked';
|
|
101
|
+
}
|
|
@@ -12,14 +12,20 @@ export function requireNoActiveStage(io) {
|
|
|
12
12
|
return { ok: false, error: `tool requires no active stage; current: ${a.stage}` };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function stageMismatchError(active, stageBase, cycle) {
|
|
16
|
+
if (stageBase && stageBaseOf(active.stage) !== stageBase) {
|
|
17
|
+
return `tool requires active ${stageBase} stage; current: ${active.stage}`;
|
|
18
|
+
}
|
|
19
|
+
if (cycle && active.cycle !== cycle) {
|
|
20
|
+
return `tool requires active stage in cycle ${cycle}; current cycle: ${active.cycle}`;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export function requireActiveStage(io, { stageBase, cycle } = {}) {
|
|
16
26
|
const a = readActiveStage(io);
|
|
17
27
|
if (!a) return { ok: false, error: `tool requires active stage; current: none` };
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
if (cycle && a.cycle !== cycle) {
|
|
22
|
-
return { ok: false, error: `tool requires active stage in cycle ${cycle}; current cycle: ${a.cycle}` };
|
|
23
|
-
}
|
|
28
|
+
const mismatch = stageMismatchError(a, stageBase, cycle);
|
|
29
|
+
if (mismatch) return { ok: false, error: mismatch };
|
|
24
30
|
return { ok: true, active: a };
|
|
25
31
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function signToken(payload, secret) {
|
|
4
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
5
|
+
const mac = createHmac('sha256', secret).update(body).digest('base64url');
|
|
6
|
+
return `${body}.${mac}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function splitToken(token) {
|
|
10
|
+
if (typeof token !== 'string' || !token.includes('.')) return null;
|
|
11
|
+
const [body, mac] = token.split('.');
|
|
12
|
+
if (!body || !mac) return null;
|
|
13
|
+
return { body, mac };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function decodeMac(mac) {
|
|
17
|
+
try { return Buffer.from(mac, 'base64url'); }
|
|
18
|
+
catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function decodePayload(body) {
|
|
22
|
+
try { return JSON.parse(Buffer.from(body, 'base64url').toString()); }
|
|
23
|
+
catch { return null; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function checkSignature(body, mac, secret) {
|
|
27
|
+
const expected = createHmac('sha256', secret).update(body).digest();
|
|
28
|
+
const given = decodeMac(mac);
|
|
29
|
+
if (!given) return { ok: false, reason: 'malformed' };
|
|
30
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
|
|
31
|
+
return { ok: false, reason: 'bad_signature' };
|
|
32
|
+
}
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkExpiry(payload) {
|
|
37
|
+
if (typeof payload.exp !== 'number' || payload.exp < Date.now()) {
|
|
38
|
+
return { ok: false, reason: 'expired' };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function verifyToken(token, secret) {
|
|
44
|
+
const parts = splitToken(token);
|
|
45
|
+
if (!parts) return { ok: false, reason: 'malformed' };
|
|
46
|
+
|
|
47
|
+
const sigCheck = checkSignature(parts.body, parts.mac, secret);
|
|
48
|
+
if (!sigCheck.ok) return sigCheck;
|
|
49
|
+
|
|
50
|
+
const payload = decodePayload(parts.body);
|
|
51
|
+
if (!payload) return { ok: false, reason: 'malformed' };
|
|
52
|
+
|
|
53
|
+
const expiryCheck = checkExpiry(payload);
|
|
54
|
+
if (!expiryCheck.ok) return expiryCheck;
|
|
55
|
+
|
|
56
|
+
return { ok: true, payload };
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL trace appender for dry-run branches.
|
|
3
|
+
*
|
|
4
|
+
* Trace files live under `.foundry/trace/<branchSlug>.jsonl`, one JSON
|
|
5
|
+
* record per line. The IO contract is async and minimal so the module
|
|
6
|
+
* stays testable with in-memory mocks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TRACE_DIR = '.foundry/trace';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert a git branch name to a filesystem-safe slug by replacing every
|
|
13
|
+
* `/` with `-`. Pure string transform — no validation, since callers pass
|
|
14
|
+
* already-validated branch names.
|
|
15
|
+
*/
|
|
16
|
+
export function branchSlug(branch) {
|
|
17
|
+
return branch.replace(/\//g, '-');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tracePath(branch) {
|
|
21
|
+
return `${TRACE_DIR}/${branchSlug(branch)}.jsonl`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Append a single JSONL record to the trace file for `branch`.
|
|
26
|
+
*
|
|
27
|
+
* Ensures the trace directory exists before writing. Uses `io.appendFile`
|
|
28
|
+
* when present, otherwise falls back to read-then-write concatenation
|
|
29
|
+
* (treating ENOENT as empty).
|
|
30
|
+
*/
|
|
31
|
+
export async function appendTraceRecord({ branch, record, io }) {
|
|
32
|
+
const path = tracePath(branch);
|
|
33
|
+
const line = JSON.stringify(record) + '\n';
|
|
34
|
+
|
|
35
|
+
await io.mkdirp(TRACE_DIR);
|
|
36
|
+
|
|
37
|
+
if (typeof io.appendFile === 'function') {
|
|
38
|
+
await io.appendFile(path, line);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let existing = '';
|
|
43
|
+
try {
|
|
44
|
+
existing = await io.readFile(path);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err && err.code !== 'ENOENT') throw err;
|
|
47
|
+
}
|
|
48
|
+
await io.writeFile(path, existing + line);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Truncate the trace file for `branch` to empty. No-op when the file
|
|
53
|
+
* does not exist.
|
|
54
|
+
*/
|
|
55
|
+
export async function truncateTrace({ branch, io }) {
|
|
56
|
+
const path = tracePath(branch);
|
|
57
|
+
if (!(await io.exists(path))) return;
|
|
58
|
+
await io.writeFile(path, '');
|
|
59
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// scripts/lib/ulid.js
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
// Crockford's base32 alphabet (excludes I, L, O, U).
|
|
5
|
+
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
6
|
+
|
|
7
|
+
// ULID spec: 10 chars of timestamp (48-bit ms since epoch) + 16 chars of randomness (80 bits).
|
|
8
|
+
// We make the randomness monotonic within the same millisecond by incrementing the previous
|
|
9
|
+
// random component by 1 whenever the timestamp hasn't advanced.
|
|
10
|
+
|
|
11
|
+
function encodeTime(ms) {
|
|
12
|
+
let out = '';
|
|
13
|
+
let remaining = ms;
|
|
14
|
+
for (let i = 9; i >= 0; i--) {
|
|
15
|
+
out = ALPHABET[remaining % 32] + out;
|
|
16
|
+
remaining = Math.floor(remaining / 32);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function randomIndexes() {
|
|
22
|
+
const bytes = randomBytes(10); // 80 bits
|
|
23
|
+
const out = new Array(16);
|
|
24
|
+
// Pack 80 bits into 16 5-bit groups.
|
|
25
|
+
let bitBuffer = 0;
|
|
26
|
+
let bits = 0;
|
|
27
|
+
let j = 0;
|
|
28
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
29
|
+
bitBuffer = bitBuffer * 256 + bytes[i];
|
|
30
|
+
bits += 8;
|
|
31
|
+
while (bits >= 5) {
|
|
32
|
+
bits -= 5;
|
|
33
|
+
out[j++] = Math.floor(bitBuffer / Math.pow(2, bits)) % 32;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function incrementRandom(arr) {
|
|
40
|
+
// Increment as a base-32 little-endian-ish counter from the right.
|
|
41
|
+
const next = arr.slice();
|
|
42
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
43
|
+
if (next[i] < 31) { next[i] += 1; return next; }
|
|
44
|
+
next[i] = 0;
|
|
45
|
+
}
|
|
46
|
+
// Overflow across all 80 bits: re-seed. Extraordinarily unlikely.
|
|
47
|
+
return randomIndexes();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates an independent ULID generator with its own monotonicity state.
|
|
52
|
+
*
|
|
53
|
+
* Monotonicity state (lastTime, lastRandom) is kept in closure, not module
|
|
54
|
+
* scope, so tests can instantiate isolated generators and production code
|
|
55
|
+
* can import a single shared instance without cross-test contamination.
|
|
56
|
+
*
|
|
57
|
+
* @returns {(now?: number) => string} generator function
|
|
58
|
+
*/
|
|
59
|
+
export function createUlidGenerator() {
|
|
60
|
+
let lastTime = 0;
|
|
61
|
+
let lastRandom = null; // array of 16 base32 char indexes
|
|
62
|
+
|
|
63
|
+
return function ulid(now = Date.now()) {
|
|
64
|
+
let randArr;
|
|
65
|
+
if (now === lastTime && lastRandom) {
|
|
66
|
+
randArr = incrementRandom(lastRandom);
|
|
67
|
+
} else {
|
|
68
|
+
randArr = randomIndexes();
|
|
69
|
+
}
|
|
70
|
+
lastTime = now;
|
|
71
|
+
lastRandom = randArr;
|
|
72
|
+
const rand = randArr.map(i => ALPHABET[i]).join('');
|
|
73
|
+
return encodeTime(now) + rand;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Default shared generator — preserves ergonomic `import { ulid }` usage.
|
|
78
|
+
// Tests that need deterministic, isolated state should call createUlidGenerator().
|
|
79
|
+
export const ulid = createUlidGenerator();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reverses encodeTime — first 10 chars of a ULID encode 48-bit ms since epoch.
|
|
83
|
+
* Returns the integer ms. Throws if any of the first 10 chars is not in the
|
|
84
|
+
* Crockford alphabet.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} id ULID string (only first 10 chars are inspected).
|
|
87
|
+
* @returns {number} milliseconds since epoch.
|
|
88
|
+
*/
|
|
89
|
+
export function decodeUlidTime(id) {
|
|
90
|
+
let time = 0;
|
|
91
|
+
for (let i = 0; i < 10; i++) {
|
|
92
|
+
const ch = id[i];
|
|
93
|
+
const idx = ALPHABET.indexOf(ch);
|
|
94
|
+
if (idx === -1) {
|
|
95
|
+
throw new Error(`decodeUlidTime: invalid Crockford base32 char '${ch}' at position ${i}`);
|
|
96
|
+
}
|
|
97
|
+
time = time * 32 + idx;
|
|
98
|
+
}
|
|
99
|
+
return time;
|
|
100
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse JSONL output from a validator, validating each line against patterns.
|
|
6
|
+
*
|
|
7
|
+
* Processes one JSON object per line. Each line must have:
|
|
8
|
+
* - file (REQUIRED): matches at least one pattern from patterns array
|
|
9
|
+
* - text (REQUIRED): feedback text
|
|
10
|
+
* - location (OPTIONAL): "line:col" format, prepended to text if present
|
|
11
|
+
* - severity (OPTIONAL): "error", "warning", etc., prepended to text if present
|
|
12
|
+
*
|
|
13
|
+
* If location and/or severity present, they are prepended to text as:
|
|
14
|
+
* [severity] file:location — <text>
|
|
15
|
+
* If only severity: [severity] file — <text>
|
|
16
|
+
* If only location: file:location — <text>
|
|
17
|
+
*
|
|
18
|
+
* Successfully parsed and pattern-matched lines flow into `items`.
|
|
19
|
+
* Errors are split into two categories so callers can distinguish them:
|
|
20
|
+
* - `parseErrors`: malformed JSON or missing required fields
|
|
21
|
+
* - `patternErrors`: file did not match any artefact-type file-pattern
|
|
22
|
+
*
|
|
23
|
+
* `ok` is true only when both error arrays are empty. Items are always
|
|
24
|
+
* returned regardless of `ok`, so a validator producing a mix of valid
|
|
25
|
+
* items and errors surfaces both.
|
|
26
|
+
*
|
|
27
|
+
* @param {Stream} stream - readable stream of JSONL lines
|
|
28
|
+
* @param {string[]} patterns - array of glob patterns for file matching
|
|
29
|
+
* @returns {Promise<{ok: boolean, items: object[], parseErrors: string[], patternErrors: string[]}>}
|
|
30
|
+
*/
|
|
31
|
+
export async function parseValidatorJsonl(stream, patterns) {
|
|
32
|
+
const items = [];
|
|
33
|
+
const parseErrors = [];
|
|
34
|
+
const patternErrors = [];
|
|
35
|
+
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const rl = readline.createInterface({
|
|
38
|
+
input: stream,
|
|
39
|
+
crlfDelay: Infinity,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let lineNum = 0;
|
|
43
|
+
|
|
44
|
+
rl.on('line', (line) => {
|
|
45
|
+
lineNum++;
|
|
46
|
+
processLine(line, lineNum, patterns, items, { parseErrors, patternErrors });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
rl.on('close', () => {
|
|
50
|
+
resolve(buildResult(items, parseErrors, patternErrors));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
rl.on('error', (err) => {
|
|
54
|
+
parseErrors.push(`Stream error: ${err.message}`);
|
|
55
|
+
resolve(buildResult(items, parseErrors, patternErrors));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build the final parse result with `ok` reflecting whether any errors occurred.
|
|
62
|
+
*/
|
|
63
|
+
function buildResult(items, parseErrors, patternErrors) {
|
|
64
|
+
const ok = parseErrors.length === 0 && patternErrors.length === 0;
|
|
65
|
+
return { ok, items, parseErrors, patternErrors };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Process a single JSONL line.
|
|
70
|
+
*
|
|
71
|
+
* `errors` bundles `parseErrors` and `patternErrors` so this function stays
|
|
72
|
+
* within the project's max-params lint budget.
|
|
73
|
+
*/
|
|
74
|
+
function processLine(line, lineNum, patterns, items, errors) {
|
|
75
|
+
const { parseErrors, patternErrors } = errors;
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
|
|
78
|
+
// Skip empty lines
|
|
79
|
+
if (!trimmed) return;
|
|
80
|
+
|
|
81
|
+
// Parse JSON
|
|
82
|
+
let obj;
|
|
83
|
+
try {
|
|
84
|
+
obj = JSON.parse(trimmed);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
parseErrors.push(`Line ${lineNum}: Invalid JSON: ${err.message}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate required fields
|
|
91
|
+
const validation = validateRequired(obj, lineNum);
|
|
92
|
+
if (validation.error) {
|
|
93
|
+
parseErrors.push(validation.error);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate file matches pattern
|
|
98
|
+
if (!fileMatchesPattern(obj.file, patterns)) {
|
|
99
|
+
patternErrors.push(`Line ${lineNum}: File '${obj.file}' does not match any pattern: ${patterns.join(', ')}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build final item with location/severity prepended if present
|
|
104
|
+
const finalItem = buildFinalItem(obj);
|
|
105
|
+
items.push(finalItem);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate required fields in parsed line.
|
|
110
|
+
*/
|
|
111
|
+
function validateRequired(obj, lineNum) {
|
|
112
|
+
if (typeof obj.file !== 'string' || obj.file.length === 0) {
|
|
113
|
+
return { error: `Line ${lineNum}: Missing required field 'file'` };
|
|
114
|
+
}
|
|
115
|
+
if (typeof obj.text !== 'string' || obj.text.length === 0) {
|
|
116
|
+
return { error: `Line ${lineNum}: Missing or empty required field 'text'` };
|
|
117
|
+
}
|
|
118
|
+
return { error: null };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if a file path matches at least one pattern.
|
|
123
|
+
*/
|
|
124
|
+
function fileMatchesPattern(file, patterns) {
|
|
125
|
+
return patterns.some(pattern => minimatch(file, pattern));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build final item with location/severity prepended to text if present.
|
|
130
|
+
*/
|
|
131
|
+
function buildFinalItem(obj) {
|
|
132
|
+
const { file, text, location, severity, ...extra } = obj;
|
|
133
|
+
const finalText = prependLocationSeverity(text, file, location, severity);
|
|
134
|
+
|
|
135
|
+
// Return all fields including extra ones
|
|
136
|
+
const result = { file, text: finalText, ...extra };
|
|
137
|
+
if (location) result.location = location;
|
|
138
|
+
if (severity) result.severity = severity;
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Prepend location and/or severity to text.
|
|
145
|
+
*/
|
|
146
|
+
function prependLocationSeverity(text, file, location, severity) {
|
|
147
|
+
if (!severity && !location) {
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let prefix = '';
|
|
152
|
+
if (severity) {
|
|
153
|
+
prefix += `[${severity}] `;
|
|
154
|
+
}
|
|
155
|
+
prefix += file;
|
|
156
|
+
if (location) {
|
|
157
|
+
prefix += `:${location}`;
|
|
158
|
+
}
|
|
159
|
+
prefix += ' — ';
|
|
160
|
+
|
|
161
|
+
return prefix + text;
|
|
162
|
+
}
|
|
@@ -8,8 +8,14 @@ import yaml from 'js-yaml';
|
|
|
8
8
|
// Frontmatter parsing
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Parse YAML frontmatter from a markdown document.
|
|
13
|
+
* NOTE: Intentionally duplicates logic from memory/frontmatter.js for
|
|
14
|
+
* different use cases. See memory/frontmatter.js for the canonical version
|
|
15
|
+
* with full error handling and line-ending normalisation.
|
|
16
|
+
*/
|
|
11
17
|
export function parseFrontmatter(text) {
|
|
12
|
-
const match = text.match(/^---\n(.+?)\n---/s);
|
|
18
|
+
const match = text.match(/^---\r?\n(.+?)\r?\n---/s);
|
|
13
19
|
if (!match) return {};
|
|
14
20
|
const fm = yaml.load(match[1]) || {};
|
|
15
21
|
// Normalize: on-disk canonical key is `max-iterations` (kebab).
|
|
@@ -24,7 +30,7 @@ export function parseFrontmatter(text) {
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export function writeFrontmatter(fields) {
|
|
27
|
-
const body = yaml.dump(fields, { lineWidth: -1 }).trimEnd();
|
|
33
|
+
const body = yaml.dump(fields, { lineWidth: -1, sortKeys: false }).trimEnd();
|
|
28
34
|
return `---\n${body}\n---`;
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -33,15 +39,21 @@ export function getFrontmatterField(text, key) {
|
|
|
33
39
|
return fm[key];
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Update a frontmatter field.
|
|
44
|
+
*
|
|
45
|
+
* Note: This function preserves key order but does not preserve YAML comments.
|
|
46
|
+
* If the frontmatter contains comments, they will be lost during rewrite.
|
|
47
|
+
*/
|
|
36
48
|
export function setFrontmatterField(text, key, value) {
|
|
37
49
|
// Coerce legacy camelCase key to canonical kebab form on write.
|
|
38
|
-
|
|
50
|
+
const normalisedKey = key === 'maxIterations' ? 'max-iterations' : key;
|
|
39
51
|
const fm = parseFrontmatter(text);
|
|
40
|
-
fm[
|
|
52
|
+
fm[normalisedKey] = value;
|
|
41
53
|
const fmBlock = writeFrontmatter(fm);
|
|
42
54
|
|
|
43
55
|
// Strip existing frontmatter (if any) and prepend new one
|
|
44
|
-
const body = text.replace(/^---\n.+?\n---\n?/s, '');
|
|
56
|
+
const body = text.replace(/^---\r?\n.+?\r?\n---\r?\n?/s, '');
|
|
45
57
|
return body ? `${fmBlock}\n${body}` : fmBlock;
|
|
46
58
|
}
|
|
47
59
|
|
|
@@ -73,19 +85,7 @@ export function parseStagesValue(raw) {
|
|
|
73
85
|
return raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
* Parse a models value from tool input.
|
|
78
|
-
* Accepts JSON object string or "key: value, key: value" string.
|
|
79
|
-
* Always returns an object mapping stage base names to model IDs.
|
|
80
|
-
*/
|
|
81
|
-
export function parseModelsValue(raw) {
|
|
82
|
-
if (!raw || !raw.trim()) return {};
|
|
83
|
-
// Try JSON first
|
|
84
|
-
try {
|
|
85
|
-
const parsed = JSON.parse(raw);
|
|
86
|
-
if (typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
87
|
-
} catch { /* not JSON */ }
|
|
88
|
-
// Fall back to "key: value, key: value" format
|
|
88
|
+
function parseKvPairs(raw) {
|
|
89
89
|
const result = {};
|
|
90
90
|
for (const part of raw.split(',')) {
|
|
91
91
|
const colonIdx = part.indexOf(':');
|
|
@@ -97,6 +97,26 @@ export function parseModelsValue(raw) {
|
|
|
97
97
|
return result;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function tryParseJsonObject(raw) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(raw);
|
|
103
|
+
if (typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
104
|
+
} catch { /* not JSON */ }
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse a models value from tool input.
|
|
110
|
+
* Accepts JSON object string or "key: value, key: value" string.
|
|
111
|
+
* Always returns an object mapping stage base names to model IDs.
|
|
112
|
+
*/
|
|
113
|
+
export function parseModelsValue(raw) {
|
|
114
|
+
if (!raw || !raw.trim()) return {};
|
|
115
|
+
const jsonResult = tryParseJsonObject(raw);
|
|
116
|
+
if (jsonResult) return jsonResult;
|
|
117
|
+
return parseKvPairs(raw);
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
// ---------------------------------------------------------------------------
|
|
101
121
|
// Workfile creation
|
|
102
122
|
// ---------------------------------------------------------------------------
|
|
@@ -110,7 +130,5 @@ ${goal}
|
|
|
110
130
|
|
|
111
131
|
| File | Type | Cycle | Status |
|
|
112
132
|
|------|------|-------|--------|
|
|
113
|
-
|
|
114
|
-
## Feedback
|
|
115
133
|
`;
|
|
116
134
|
}
|