@really-knows-ai/foundry 2.3.1 → 3.0.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/README.md +200 -198
- 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 +490 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +434 -0
- package/dist/docs/concepts.md +396 -0
- package/dist/docs/getting-started.md +345 -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 +393 -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 +42 -6
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +49 -21
- package/{skills → dist/skills}/add-cycle/SKILL.md +60 -14
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +39 -7
- 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 +192 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -90
- package/docs/concepts.md +0 -59
- package/docs/getting-started.md +0 -78
- package/docs/work-spec.md +0 -193
- 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 -105
- 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,186 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readActiveStage, writeActiveStage, clearActiveStage, writeLastStage, clearLastStage } from '../../../scripts/lib/state.js';
|
|
4
|
+
import { verifyToken } from '../../../scripts/lib/token.js';
|
|
5
|
+
import { readOrCreateSecret } from '../../../scripts/lib/secret.js';
|
|
6
|
+
import { getContext, invalidateStore } from '../../../scripts/lib/memory/singleton.js';
|
|
7
|
+
import { syncStore } from '../../../scripts/lib/memory/store.js';
|
|
8
|
+
import { makeIO, makeMemoryIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
9
|
+
import { markWorkfileFailed, readFailedStatus, clearWorkfileFailed } from '../../../scripts/lib/failed-flow.js';
|
|
10
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
11
|
+
|
|
12
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
13
|
+
|
|
14
|
+
// -- Helpers for foundry_stage_begin --
|
|
15
|
+
|
|
16
|
+
function resolveBaseSha(worktree) {
|
|
17
|
+
try {
|
|
18
|
+
return execSync('git rev-parse HEAD', { cwd: worktree }).toString().trim();
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function verifyStageToken(token, secret, stage, cycle) {
|
|
25
|
+
const v = verifyToken(token, secret);
|
|
26
|
+
if (!v.ok) return { error: `foundry_stage_begin: token ${v.reason}` };
|
|
27
|
+
if (v.payload.route !== stage || v.payload.cycle !== cycle) {
|
|
28
|
+
return { error: `foundry_stage_begin: token payload mismatch (route=${v.payload.route}, cycle=${v.payload.cycle})` };
|
|
29
|
+
}
|
|
30
|
+
return { payload: v.payload };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function executeStageBegin(args, context, pending) {
|
|
34
|
+
const io = makeIO(context.worktree);
|
|
35
|
+
const secret = readOrCreateSecret(context.worktree);
|
|
36
|
+
|
|
37
|
+
const current = readActiveStage(io);
|
|
38
|
+
if (current) {
|
|
39
|
+
return JSON.stringify({ error: `foundry_stage_begin requires no active stage; current: ${current.stage}` });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tokenResult = verifyStageToken(args.token, secret, args.stage, args.cycle);
|
|
43
|
+
if (tokenResult.error) return JSON.stringify({ error: tokenResult.error });
|
|
44
|
+
|
|
45
|
+
const baseSha = resolveBaseSha(context.worktree);
|
|
46
|
+
if (!baseSha) {
|
|
47
|
+
return JSON.stringify({ error: 'foundry_stage_begin: git rev-parse HEAD failed (no commits?)' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const meta = pending.consume(tokenResult.payload.nonce);
|
|
51
|
+
if (!meta) return JSON.stringify({ error: 'foundry_stage_begin: nonce not pending or already consumed' });
|
|
52
|
+
|
|
53
|
+
const tokenHash = createHash('sha256').update(args.token).digest('hex');
|
|
54
|
+
const active = {
|
|
55
|
+
cycle: args.cycle,
|
|
56
|
+
stage: args.stage,
|
|
57
|
+
tokenHash,
|
|
58
|
+
baseSha,
|
|
59
|
+
startedAt: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
writeActiveStage(io, active);
|
|
62
|
+
return JSON.stringify({ ok: true, active });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -- Helpers for foundry_stage_end --
|
|
66
|
+
|
|
67
|
+
function markWorkfileFailedSilently(io, msg) {
|
|
68
|
+
try { markWorkfileFailed(io, msg); } catch { /* WORK.md gone */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function syncMemoryAtStageEnd(worktree) {
|
|
72
|
+
const memIo = makeMemoryIO(worktree);
|
|
73
|
+
const ctx = getContext(worktree);
|
|
74
|
+
if (ctx && ctx.store) {
|
|
75
|
+
await syncStore({ store: ctx.store, io: memIo });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function executeStageEnd(args, context) {
|
|
80
|
+
const io = makeIO(context.worktree);
|
|
81
|
+
const active = readActiveStage(io);
|
|
82
|
+
if (!active) {
|
|
83
|
+
return JSON.stringify({ error: 'foundry_stage_end requires active stage; current: none' });
|
|
84
|
+
}
|
|
85
|
+
writeLastStage(io, {
|
|
86
|
+
cycle: active.cycle,
|
|
87
|
+
stage: active.stage,
|
|
88
|
+
baseSha: active.baseSha,
|
|
89
|
+
summary: args.summary,
|
|
90
|
+
});
|
|
91
|
+
clearActiveStage(io);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await syncMemoryAtStageEnd(context.worktree);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
97
|
+
const msg = `memory sync at stage end failed: ${detail}`;
|
|
98
|
+
markWorkfileFailedSilently(io, msg);
|
|
99
|
+
return JSON.stringify({ error: msg, flow_failed: true });
|
|
100
|
+
}
|
|
101
|
+
return JSON.stringify({ ok: true, summary: args.summary });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// -- Helpers for foundry_stage_retry --
|
|
105
|
+
|
|
106
|
+
function checkGitWorkingTreeClean(worktree) {
|
|
107
|
+
try {
|
|
108
|
+
const statusOut = execSync('git status --porcelain', { cwd: worktree }).toString();
|
|
109
|
+
return statusOut.trim() === '' ? { ok: true } : { ok: false };
|
|
110
|
+
} catch {
|
|
111
|
+
return { ok: false };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function executeStageRetry(_args, context) {
|
|
116
|
+
const io = makeIO(context.worktree);
|
|
117
|
+
|
|
118
|
+
const failed = readFailedStatus(io);
|
|
119
|
+
if (!failed) {
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
ok: false,
|
|
122
|
+
error: 'foundry_stage_retry requires failed flow; current status is not failed',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const active = readActiveStage(io);
|
|
127
|
+
if (active) {
|
|
128
|
+
return JSON.stringify({
|
|
129
|
+
ok: false,
|
|
130
|
+
error: 'foundry_stage_retry requires no active stage; call foundry_stage_end first',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const gitCheck = checkGitWorkingTreeClean(context.worktree);
|
|
135
|
+
if (!gitCheck.ok) {
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
ok: false,
|
|
138
|
+
error: 'foundry_stage_retry requires clean git working tree; commit or stash changes first',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
invalidateStore(context.worktree);
|
|
143
|
+
|
|
144
|
+
try { clearLastStage(io); } catch { /* last-stage.json might not exist */ }
|
|
145
|
+
|
|
146
|
+
clearWorkfileFailed(io);
|
|
147
|
+
|
|
148
|
+
return JSON.stringify({
|
|
149
|
+
ok: true,
|
|
150
|
+
message: 'Flow unlocked. Memory state reset to disk. Stage can be re-run.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createStageTools({ tool, pending }) {
|
|
155
|
+
return {
|
|
156
|
+
foundry_stage_begin: tool({
|
|
157
|
+
description: 'Open a subagent work stage; consumes a dispatch token from foundry_orchestrate.',
|
|
158
|
+
args: {
|
|
159
|
+
stage: tool.schema.string().describe('Stage alias, e.g. "forge:create-haiku"'),
|
|
160
|
+
cycle: tool.schema.string().describe('Cycle name'),
|
|
161
|
+
token: tool.schema.string().describe('Token received from foundry_orchestrate via the dispatch prompt'),
|
|
162
|
+
},
|
|
163
|
+
execute: guarded('foundry_stage_begin', [flowBranchGuard, gateNotFailed],
|
|
164
|
+
(args, context) => executeStageBegin(args, context, pending),
|
|
165
|
+
{ branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
foundry_stage_end: tool({
|
|
169
|
+
description: 'Close the active subagent work stage; preserves baseSha for finalize.',
|
|
170
|
+
args: {
|
|
171
|
+
summary: tool.schema.string().describe('Short summary of the work done'),
|
|
172
|
+
},
|
|
173
|
+
execute: guarded('foundry_stage_end', [flowBranchGuard],
|
|
174
|
+
executeStageEnd,
|
|
175
|
+
{ branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
176
|
+
}),
|
|
177
|
+
|
|
178
|
+
foundry_stage_retry: tool({
|
|
179
|
+
description: 'Retry a failed stage by discarding uncommitted memory changes and clearing the failed state. Requires clean git working tree.',
|
|
180
|
+
args: {},
|
|
181
|
+
execute: guarded('foundry_stage_retry', [flowBranchGuard],
|
|
182
|
+
executeStageRetry,
|
|
183
|
+
{ branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { readdir } from 'fs/promises';
|
|
3
|
+
import { join, relative, sep } from 'path';
|
|
4
|
+
import { minimatch } from 'minimatch';
|
|
5
|
+
import { getLawsForQuench, getArtefactType } from '../../../scripts/lib/config.js';
|
|
6
|
+
import { parseValidatorJsonl } from '../../../scripts/lib/validator-jsonl.js';
|
|
7
|
+
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
8
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
9
|
+
|
|
10
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shell-quote a string for POSIX `/bin/sh` so it is treated as a single literal
|
|
14
|
+
* argument. Wraps the value in single quotes and escapes any embedded single
|
|
15
|
+
* quotes via the `'\''` idiom. Safe for arbitrary file paths including ones
|
|
16
|
+
* containing spaces, semicolons, `$()`, backticks, quotes, and newlines.
|
|
17
|
+
*/
|
|
18
|
+
function shellQuote(value) {
|
|
19
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute a validator command and parse its output.
|
|
24
|
+
*/
|
|
25
|
+
async function executeValidator(expanded, worktree, patterns) {
|
|
26
|
+
try {
|
|
27
|
+
const output = execSync(expanded, {
|
|
28
|
+
cwd: worktree,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
+
});
|
|
32
|
+
const { Readable } = await import('stream');
|
|
33
|
+
const stream = Readable.from([output]);
|
|
34
|
+
return await parseValidatorJsonl(stream, patterns);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// Validator command failed - prefer stdout for JSONL (tools like rg exit 1 with results on stdout)
|
|
37
|
+
const output = (err.stdout || err.stderr || err.message || '').trim();
|
|
38
|
+
const { Readable } = await import('stream');
|
|
39
|
+
const stream = Readable.from([output]);
|
|
40
|
+
return await parseValidatorJsonl(stream, patterns);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run all validators for laws and collect results.
|
|
46
|
+
*
|
|
47
|
+
* Aggregates per-validator parse results into a single structured payload:
|
|
48
|
+
* - `items`: each successfully parsed feedback item, annotated with the
|
|
49
|
+
* `lawId` and `validatorId` it came from. The quench skill consumes this
|
|
50
|
+
* to call `foundry_feedback_add` with tag `law:<lawId>:<validatorId>`.
|
|
51
|
+
* - `errors`: validator-level errors split by type. `parse` for malformed
|
|
52
|
+
* JSON or missing required fields, `pattern-mismatch` for files that
|
|
53
|
+
* didn't match the artefact type's `file-patterns`.
|
|
54
|
+
*/
|
|
55
|
+
async function runValidators(laws, patterns, patternSubstitution, worktree) {
|
|
56
|
+
const results = {
|
|
57
|
+
validatorsRun: 0,
|
|
58
|
+
items: [],
|
|
59
|
+
errors: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (const law of laws) {
|
|
63
|
+
if (!law.validators || law.validators.length === 0) continue;
|
|
64
|
+
await runLawValidators(law, patterns, patternSubstitution, worktree, results);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run validators for a single law.
|
|
72
|
+
*/
|
|
73
|
+
async function runLawValidators(law, patterns, patternSubstitution, worktree, results) {
|
|
74
|
+
for (const validator of law.validators) {
|
|
75
|
+
// Skip validators if pattern substitution is empty (no matching files)
|
|
76
|
+
// Self-resolving validators (npm test, tsc) omit {pattern}, so they still run
|
|
77
|
+
if (patternSubstitution === '' && validator.command.includes('{pattern}')) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
results.validatorsRun++;
|
|
81
|
+
const expanded = expandValidatorCommand(validator.command, patternSubstitution);
|
|
82
|
+
const parseResult = await executeValidator(expanded, worktree, patterns);
|
|
83
|
+
collectValidatorResult(parseResult, law.id, validator.id, results);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fold a single validator's parse result into the aggregate results.
|
|
89
|
+
*
|
|
90
|
+
* Items always flow through annotated with their `lawId` and `validatorId`,
|
|
91
|
+
* so the caller can construct `law:<lawId>:<validatorId>` feedback tags.
|
|
92
|
+
* Errors are surfaced with their type so the caller can distinguish parse
|
|
93
|
+
* failures from file-pattern mismatches.
|
|
94
|
+
*/
|
|
95
|
+
function collectValidatorResult(parseResult, lawId, validatorId, results) {
|
|
96
|
+
for (const item of parseResult.items) {
|
|
97
|
+
results.items.push({ lawId, validatorId, ...item });
|
|
98
|
+
}
|
|
99
|
+
for (const message of parseResult.parseErrors) {
|
|
100
|
+
results.errors.push({ lawId, validatorId, type: 'parse', message });
|
|
101
|
+
}
|
|
102
|
+
for (const message of parseResult.patternErrors) {
|
|
103
|
+
results.errors.push({ lawId, validatorId, type: 'pattern-mismatch', message });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createValidateTools({ tool }) {
|
|
108
|
+
return {
|
|
109
|
+
foundry_validate_run: tool({
|
|
110
|
+
description: 'Run validation commands for an artefact type. Returns parsed feedback items per validator with their law and validator IDs so the caller can tag feedback as law:<law-id>:<validator-id>.',
|
|
111
|
+
args: {
|
|
112
|
+
typeId: tool.schema.string().describe('Artefact type ID'),
|
|
113
|
+
},
|
|
114
|
+
execute: guarded('foundry_validate_run', [flowBranchGuard, gateNotFailed],
|
|
115
|
+
executeValidateRun,
|
|
116
|
+
{ branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function executeValidateRun(args, context) {
|
|
122
|
+
try {
|
|
123
|
+
return await performValidation(args, context);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return JSON.stringify({ ok: false, error: `foundry_validate_run: ${err.message}` });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Perform actual validation work.
|
|
131
|
+
*/
|
|
132
|
+
async function getValidationPatterns(foundryDir, typeId, io) {
|
|
133
|
+
const artType = await getArtefactType(foundryDir, typeId, io);
|
|
134
|
+
return artType.frontmatter['file-patterns'] || [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function performValidation(args, context) {
|
|
138
|
+
const io = makeIO(context.worktree);
|
|
139
|
+
const foundryDir = join(context.worktree, 'foundry');
|
|
140
|
+
|
|
141
|
+
let patterns;
|
|
142
|
+
try {
|
|
143
|
+
patterns = await getValidationPatterns(foundryDir, args.typeId, io);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return JSON.stringify({ ok: false, error: err.message });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const validationErr = validatePatterns(patterns, args.typeId);
|
|
149
|
+
if (validationErr) return JSON.stringify(validationErr);
|
|
150
|
+
|
|
151
|
+
const laws = await getLawsForQuench(foundryDir, io, { typeId: args.typeId });
|
|
152
|
+
if (!laws?.length) {
|
|
153
|
+
return JSON.stringify({ ok: true, validatorsRun: 0, items: [], errors: [] });
|
|
154
|
+
}
|
|
155
|
+
return runValidatorsAndReport(laws, patterns, context.worktree);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Run validators and report results.
|
|
160
|
+
*/
|
|
161
|
+
async function runValidatorsAndReport(laws, patterns, worktree) {
|
|
162
|
+
const expandedFiles = await expandPatterns(patterns, worktree);
|
|
163
|
+
const patternSubstitution = expandedFiles.map(shellQuote).join(' ');
|
|
164
|
+
const results = await runValidators(laws, patterns, patternSubstitution, worktree);
|
|
165
|
+
|
|
166
|
+
return JSON.stringify({
|
|
167
|
+
ok: results.errors.length === 0,
|
|
168
|
+
validatorsRun: results.validatorsRun,
|
|
169
|
+
items: results.items,
|
|
170
|
+
errors: results.errors,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Validate file patterns.
|
|
176
|
+
*/
|
|
177
|
+
function validatePatterns(patterns, typeId) {
|
|
178
|
+
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
179
|
+
return { ok: false, error: `Artefact type ${typeId} has no file-patterns` };
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const SKIP_DIRS = new Set(['node_modules', '.git']);
|
|
185
|
+
|
|
186
|
+
function toPosix(p) {
|
|
187
|
+
return sep === '/' ? p : p.split(sep).join('/');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function readdirSafe(dir) {
|
|
191
|
+
try {
|
|
192
|
+
return await readdir(dir, { withFileTypes: true });
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Recursively walk `dir` and yield POSIX-style paths relative to `root`.
|
|
200
|
+
* Skips `node_modules` and `.git` for speed; the artefacts we validate live
|
|
201
|
+
* elsewhere.
|
|
202
|
+
*/
|
|
203
|
+
async function* walkFiles(root, dir) {
|
|
204
|
+
for (const entry of await readdirSafe(dir)) {
|
|
205
|
+
const full = join(dir, entry.name);
|
|
206
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
207
|
+
yield* walkFiles(root, full);
|
|
208
|
+
} else if (entry.isFile()) {
|
|
209
|
+
yield toPosix(relative(root, full));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fileMatchesAnyPattern(rel, patterns) {
|
|
215
|
+
for (const pattern of patterns) {
|
|
216
|
+
if (minimatch(rel, pattern)) return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Expand glob patterns to actual files in the worktree.
|
|
223
|
+
*
|
|
224
|
+
* Implemented over `readdir` + `minimatch` so we work on Node 20, which lacks
|
|
225
|
+
* `fs/promises.glob` (added in Node 22).
|
|
226
|
+
*/
|
|
227
|
+
async function expandPatterns(patterns, worktree) {
|
|
228
|
+
const files = new Set();
|
|
229
|
+
for await (const rel of walkFiles(worktree, worktree)) {
|
|
230
|
+
if (fileMatchesAnyPattern(rel, patterns)) {
|
|
231
|
+
files.add(rel);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return Array.from(files).sort();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Expand validator command by replacing {pattern} placeholder.
|
|
239
|
+
*
|
|
240
|
+
* Only replaces {pattern} when it appears as a standalone token bounded by
|
|
241
|
+
* whitespace or string start/end. This allows self-resolving validators
|
|
242
|
+
* (e.g., npm test, tsc --noEmit) to omit the placeholder without risk of
|
|
243
|
+
* accidental substitution if they contain the literal text "{pattern}" as part
|
|
244
|
+
* of another string.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} command - The validator command
|
|
247
|
+
* @param {string} patternSubstitution - Shell-quoted file paths, space-separated
|
|
248
|
+
* @returns {string} The expanded command
|
|
249
|
+
*/
|
|
250
|
+
export function expandValidatorCommand(command, patternSubstitution) {
|
|
251
|
+
// First strip surrounding quotes around {pattern} to handle cases like
|
|
252
|
+
// rg "{pattern}" where authors add quotes for readability
|
|
253
|
+
const cmd = command
|
|
254
|
+
.replace(/"\{pattern\}"/g, '{pattern}')
|
|
255
|
+
.replace(/'\{pattern\}'/g, '{pattern}');
|
|
256
|
+
|
|
257
|
+
// Only substitute {pattern} when it appears as a standalone token
|
|
258
|
+
// (bounded by whitespace or start/end of string)
|
|
259
|
+
return cmd.replace(/(?:^|\s)\{pattern\}(?=\s|$)/g, (match) => {
|
|
260
|
+
const leadingSpace = match.startsWith('{') ? '' : ' ';
|
|
261
|
+
return leadingSpace + patternSubstitution;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { requireNoActiveStage } from '../../../scripts/lib/stage-guard.js';
|
|
4
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
5
|
+
import { parseFrontmatter, createWorkfile, enrichStages, parseModelsValue } from '../../../scripts/lib/workfile.js';
|
|
6
|
+
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
7
|
+
|
|
8
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
9
|
+
|
|
10
|
+
function buildFrontmatter(args) {
|
|
11
|
+
const fm = { flow: args.flow, cycle: args.cycle };
|
|
12
|
+
if (args.stages) {
|
|
13
|
+
fm.stages = enrichStages(args.stages, args.cycle);
|
|
14
|
+
}
|
|
15
|
+
if (args.maxIterations !== undefined) {
|
|
16
|
+
fm['max-iterations'] = args.maxIterations;
|
|
17
|
+
}
|
|
18
|
+
if (args.models) {
|
|
19
|
+
fm.models = parseModelsValue(args.models);
|
|
20
|
+
}
|
|
21
|
+
return fm;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function deleteIfExists(...paths) {
|
|
25
|
+
for (const p of paths) {
|
|
26
|
+
if (existsSync(p)) {
|
|
27
|
+
unlinkSync(p);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function executeWorkfileCreate(args, context) {
|
|
33
|
+
const io = makeIO(context.worktree);
|
|
34
|
+
const guard = requireNoActiveStage(io);
|
|
35
|
+
if (!guard.ok) return JSON.stringify({ error: `foundry_workfile_create ${guard.error}` });
|
|
36
|
+
const workPath = path.join(context.worktree, 'WORK.md');
|
|
37
|
+
if (existsSync(workPath)) {
|
|
38
|
+
return JSON.stringify({ error: 'foundry_workfile_create requires no WORK.md; current: exists' });
|
|
39
|
+
}
|
|
40
|
+
const fm = buildFrontmatter(args);
|
|
41
|
+
const content = createWorkfile(fm, args.goal);
|
|
42
|
+
writeFileSync(workPath, content, 'utf-8');
|
|
43
|
+
return JSON.stringify({ ok: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function executeWorkfileDelete(args, context) {
|
|
47
|
+
const io = makeIO(context.worktree);
|
|
48
|
+
const guard = requireNoActiveStage(io);
|
|
49
|
+
if (!guard.ok) return JSON.stringify({ error: `foundry_workfile_delete ${guard.error}` });
|
|
50
|
+
if (args.confirm !== true) {
|
|
51
|
+
return JSON.stringify({ error: 'foundry_workfile_delete requires {confirm: true}' });
|
|
52
|
+
}
|
|
53
|
+
const workPath = path.join(context.worktree, 'WORK.md');
|
|
54
|
+
const historyPath = path.join(context.worktree, 'WORK.history.yaml');
|
|
55
|
+
const feedbackPath = path.join(context.worktree, 'WORK.feedback.yaml');
|
|
56
|
+
deleteIfExists(workPath, historyPath, feedbackPath);
|
|
57
|
+
return JSON.stringify({ ok: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createWorkfileTools({ tool }) {
|
|
61
|
+
return {
|
|
62
|
+
foundry_workfile_create: tool({
|
|
63
|
+
description: 'Create WORK.md with frontmatter and goal',
|
|
64
|
+
args: {
|
|
65
|
+
flow: tool.schema.string().describe('Flow name'),
|
|
66
|
+
cycle: tool.schema.string().describe('Cycle name'),
|
|
67
|
+
stages: tool.schema.array(tool.schema.string()).optional().describe('Ordered stage names'),
|
|
68
|
+
maxIterations: tool.schema.number().optional().describe('Maximum iterations'),
|
|
69
|
+
goal: tool.schema.string().describe('Goal text'),
|
|
70
|
+
models: tool.schema.string().optional().describe('Per-stage model overrides as JSON object, e.g. \'{"forge":"openai/gpt-4o"}\''),
|
|
71
|
+
},
|
|
72
|
+
execute: guarded('foundry_workfile_create', [flowBranchGuard, gateNotFailed], executeWorkfileCreate, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
foundry_workfile_get: tool({
|
|
76
|
+
description: 'Read WORK.md and return frontmatter + goal',
|
|
77
|
+
args: {},
|
|
78
|
+
async execute(_args, context) {
|
|
79
|
+
const workPath = path.join(context.worktree, 'WORK.md');
|
|
80
|
+
if (!existsSync(workPath)) {
|
|
81
|
+
return JSON.stringify({ error: 'WORK.md not found' });
|
|
82
|
+
}
|
|
83
|
+
const text = readFileSync(workPath, 'utf-8');
|
|
84
|
+
const fm = parseFrontmatter(text);
|
|
85
|
+
const goalMatch = text.match(/# Goal\n\n([\s\S]*?)(?=\n\||\n##|$)/);
|
|
86
|
+
const goal = goalMatch ? goalMatch[1].trim() : '';
|
|
87
|
+
return JSON.stringify({ ...fm, goal });
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
foundry_workfile_delete: tool({
|
|
92
|
+
description: 'Delete WORK.md, WORK.history.yaml, and WORK.feedback.yaml (requires confirm:true)',
|
|
93
|
+
args: {
|
|
94
|
+
confirm: tool.schema.boolean().describe('Must be true to confirm deletion'),
|
|
95
|
+
},
|
|
96
|
+
// Branch guard only: workfile_delete is the failed-flow escape hatch,
|
|
97
|
+
// so it must remain callable when WORK.md has status: failed under
|
|
98
|
+
// the branch guard alone.
|
|
99
|
+
execute: guarded('foundry_workfile_delete', [flowBranchGuard], executeWorkfileDelete, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foundry plugin for OpenCode.ai
|
|
3
|
+
*
|
|
4
|
+
* All skills are always registered. Individual skills check for foundry/ dir.
|
|
5
|
+
* - If foundry/ exists: pipeline context injected into first message
|
|
6
|
+
* - If foundry/ does not exist: minimal prompt guiding user to init-foundry
|
|
7
|
+
* Multi-model agents are managed as .opencode/agents/foundry-*.md files via the refresh-agents skill.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { tool } from '@opencode-ai/plugin';
|
|
13
|
+
import { createPendingStore } from '../../scripts/lib/pending.js';
|
|
14
|
+
import { getBootstrapContent } from './foundry-tools/helpers.js';
|
|
15
|
+
import { createHistoryTools } from './foundry-tools/history-tools.js';
|
|
16
|
+
import { createStageTools } from './foundry-tools/stage-tools.js';
|
|
17
|
+
import { createWorkfileTools } from './foundry-tools/workfile-tools.js';
|
|
18
|
+
import { createOrchestrateTool } from './foundry-tools/orchestrate-tool.js';
|
|
19
|
+
import { createArtefactTools } from './foundry-tools/artefact-tools.js';
|
|
20
|
+
import { createFeedbackTools } from './foundry-tools/feedback-tools.js';
|
|
21
|
+
import { createGitTools } from './foundry-tools/git-tools.js';
|
|
22
|
+
import { createConfigTools } from './foundry-tools/config-tools.js';
|
|
23
|
+
import { createConfigCreateTools } from './foundry-tools/config-create-tools.js';
|
|
24
|
+
import { createConfigLawTools } from './foundry-tools/config-law-tools.js';
|
|
25
|
+
import { createValidateTools } from './foundry-tools/validate-tools.js';
|
|
26
|
+
import { createAssayTools } from './foundry-tools/assay-tools.js';
|
|
27
|
+
import { createAppraiserTools } from './foundry-tools/appraiser-tools.js';
|
|
28
|
+
import { createMemoryTools } from './foundry-tools/memory-tools.js';
|
|
29
|
+
import { createMemoryAdminTools } from './foundry-tools/memory-admin-tools.js';
|
|
30
|
+
import { createSnapshotTools } from './foundry-tools/snapshot-tools.js';
|
|
31
|
+
import { createAttestationTools } from './foundry-tools/attestation-tools.js';
|
|
32
|
+
|
|
33
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const packageRoot = path.resolve(__dirname, '../..');
|
|
35
|
+
const allSkillsDir = path.join(packageRoot, 'skills');
|
|
36
|
+
|
|
37
|
+
export { buildCyclePromptExtras } from './foundry-tools/helpers.js';
|
|
38
|
+
|
|
39
|
+
function buildTools(createTool, pending) {
|
|
40
|
+
return {
|
|
41
|
+
...createHistoryTools({ tool: createTool }),
|
|
42
|
+
...createStageTools({ tool: createTool, pending }),
|
|
43
|
+
...createWorkfileTools({ tool: createTool }),
|
|
44
|
+
...createOrchestrateTool({ tool: createTool, pending }),
|
|
45
|
+
...createArtefactTools({ tool: createTool }),
|
|
46
|
+
...createFeedbackTools({ tool: createTool }),
|
|
47
|
+
...createGitTools({ tool: createTool }),
|
|
48
|
+
...createConfigTools({ tool: createTool }),
|
|
49
|
+
...createConfigCreateTools({ tool: createTool }),
|
|
50
|
+
...createConfigLawTools({ tool: createTool }),
|
|
51
|
+
...createValidateTools({ tool: createTool }),
|
|
52
|
+
...createAssayTools({ tool: createTool }),
|
|
53
|
+
...createAppraiserTools({ tool: createTool }),
|
|
54
|
+
...createMemoryTools({ tool: createTool }),
|
|
55
|
+
...createMemoryAdminTools({ tool: createTool }),
|
|
56
|
+
...createSnapshotTools({ tool: createTool }),
|
|
57
|
+
...createAttestationTools({ tool: createTool }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasFoundryContext(parts) {
|
|
62
|
+
return parts.some(p => p.type === 'text' && p.text.includes('FOUNDRY_CONTEXT'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFirstUserWithParts(output) {
|
|
66
|
+
if (!output.messages.length) return null;
|
|
67
|
+
const firstUser = output.messages.find(m => m.info.role === 'user');
|
|
68
|
+
if (!firstUser || !firstUser.parts.length) return null;
|
|
69
|
+
return firstUser;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const FoundryPlugin = async ({ directory }) => {
|
|
73
|
+
// Pending store is per-plugin-instance (shared across all tool invocations).
|
|
74
|
+
const pending = createPendingStore();
|
|
75
|
+
|
|
76
|
+
const plugin = {
|
|
77
|
+
config: async (config) => {
|
|
78
|
+
config.skills = config.skills || {};
|
|
79
|
+
config.skills.paths = config.skills.paths || [];
|
|
80
|
+
|
|
81
|
+
// Always register all skills — individual skills check for foundry/ dir
|
|
82
|
+
if (!config.skills.paths.includes(allSkillsDir)) {
|
|
83
|
+
config.skills.paths.push(allSkillsDir);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
'experimental.chat.messages.transform': async (_input, output) => {
|
|
88
|
+
const bootstrap = getBootstrapContent(directory, packageRoot);
|
|
89
|
+
if (!bootstrap) return;
|
|
90
|
+
|
|
91
|
+
const firstUser = getFirstUserWithParts(output);
|
|
92
|
+
if (!firstUser) return;
|
|
93
|
+
|
|
94
|
+
if (hasFoundryContext(firstUser.parts)) return;
|
|
95
|
+
|
|
96
|
+
const ref = firstUser.parts[0];
|
|
97
|
+
firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
tool: buildTools(tool, pending),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
Object.defineProperty(plugin, Symbol.for('foundry.test.pending'), { value: pending });
|
|
104
|
+
return plugin;
|
|
105
|
+
};
|