@really-knows-ai/foundry 2.3.2 → 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 +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 +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 +39 -9
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +46 -24
- 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 +192 -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,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work branch finisher — reads ATTEST.md, verifies diff SHA, squash-merges.
|
|
3
|
+
*
|
|
4
|
+
* ATTEST.md must exist, must be the HEAD commit, and its diff-sha256 must match
|
|
5
|
+
* a recomputed SHA of the branch diff (merge-base to HEAD~1).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sha256Buffer } from '../attestation/hash.js';
|
|
9
|
+
|
|
10
|
+
/** Run git command; return { ok, result } or { ok: false, error }. */
|
|
11
|
+
function safeGit(execGit, args) {
|
|
12
|
+
try {
|
|
13
|
+
return { ok: true, result: execGit(args).trim() };
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return { ok: false, error: err };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Best-effort git call — swallows all errors. */
|
|
20
|
+
function bestEffort(execGit, args) {
|
|
21
|
+
try { execGit(args); } catch { /* best effort */ }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Best-effort rollback: reset, restore branch, delete archive, remove temp file. */
|
|
25
|
+
function rollback({ execGit, branchName, archiveBranch, commitFile, deleteFile }) {
|
|
26
|
+
bestEffort(execGit, ['reset', '--merge']);
|
|
27
|
+
bestEffort(execGit, ['checkout', branchName]);
|
|
28
|
+
bestEffort(execGit, ['branch', '-D', archiveBranch]);
|
|
29
|
+
if (deleteFile && commitFile) bestEffort(() => deleteFile(commitFile), []);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Extract stderr or stdout string from a caught error. */
|
|
33
|
+
function extractStderr(err) {
|
|
34
|
+
if (!err) return '';
|
|
35
|
+
if (err.stderr) return String(err.stderr).trim();
|
|
36
|
+
if (err.stdout) return String(err.stdout).trim();
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Build an error string from a caught git error. */
|
|
41
|
+
function gitError(prefix, err) {
|
|
42
|
+
const msg = extractStderr(err);
|
|
43
|
+
if (msg) return prefix + ' ' + msg;
|
|
44
|
+
return prefix;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse the diff-sha256 line from ATTEST.md content. */
|
|
48
|
+
function parseDiffSha(content) {
|
|
49
|
+
const match = content.match(/^diff-sha256:\s*([0-9a-f]{64})/m);
|
|
50
|
+
return match ? match[1] : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Compute the SHA of the branch diff (merge-base to HEAD~1). */
|
|
54
|
+
function computeDiffSha(execGit, baseBranch) {
|
|
55
|
+
const mergeBase = execGit(['merge-base', 'HEAD', baseBranch]).trim();
|
|
56
|
+
const diffOutput = execGit(['diff', mergeBase, 'HEAD~1']);
|
|
57
|
+
const diffBuf = Buffer.isBuffer(diffOutput) ? diffOutput : Buffer.from(diffOutput, 'utf8');
|
|
58
|
+
return sha256Buffer(diffBuf);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Validate ATTEST.md exists and HEAD is the attest commit. */
|
|
62
|
+
function checkAttestFile(cwd, fileExists, execGit) {
|
|
63
|
+
const attestPath = `${cwd}/ATTEST.md`;
|
|
64
|
+
if (!fileExists(attestPath)) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: 'foundry_git_finish: ATTEST.md not found. Run foundry_attest before finishing the branch.',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const headLine = execGit(['log', '--oneline', '-1']).trim();
|
|
71
|
+
if (!headLine.includes('attest: cycle complete')) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: `foundry_git_finish: HEAD commit is not the attest commit. Run foundry_attest first. HEAD: ${headLine}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, attestPath };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Validate the diff SHA in ATTEST.md matches the recomputed branch diff. */
|
|
81
|
+
function checkDiffSha(attestPath, readAttest, execGit, baseBranch) {
|
|
82
|
+
const attestContent = readAttest(attestPath);
|
|
83
|
+
const recordedDiffSha = parseDiffSha(attestContent);
|
|
84
|
+
if (!recordedDiffSha) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: 'foundry_git_finish: ATTEST.md is malformed — no valid diff-sha256 line found.',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const computedDiffSha = computeDiffSha(execGit, baseBranch);
|
|
91
|
+
if (computedDiffSha !== recordedDiffSha) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: `foundry_git_finish: diff SHA mismatch. Recorded: ${recordedDiffSha}. Computed: ${computedDiffSha}. The branch has changed since attestation.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, attestContent };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Checkout the base branch; roll back on failure. */
|
|
101
|
+
function checkoutBase(execGit, baseBranch, branchName, archiveBranch, deleteFile) {
|
|
102
|
+
const res = safeGit(execGit, ['checkout', baseBranch]);
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
rollback({ execGit, branchName, archiveBranch, deleteFile });
|
|
105
|
+
return { ok: false, error: gitError('foundry_git_finish: checkout base branch failed.', res.error) };
|
|
106
|
+
}
|
|
107
|
+
return { ok: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Squash-merge the work branch; roll back on failure. */
|
|
111
|
+
function squashMerge(execGit, branchName, branchNameOrig, archiveBranch, deleteFile) {
|
|
112
|
+
const res = safeGit(execGit, ['merge', '--squash', branchName]);
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
rollback({ execGit, branchName: branchNameOrig, archiveBranch, deleteFile });
|
|
115
|
+
return { ok: false, error: gitError('foundry_git_finish: squash merge failed.', res.error) };
|
|
116
|
+
}
|
|
117
|
+
return { ok: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Write the commit message file; roll back on failure. */
|
|
121
|
+
function writeCommitMsg({ writeTempMessage, attestContent, execGit, branchName, archiveBranch, deleteFile }) {
|
|
122
|
+
try {
|
|
123
|
+
return { ok: true, commitFile: writeTempMessage(attestContent) };
|
|
124
|
+
} catch (err) {
|
|
125
|
+
rollback({ execGit, branchName, archiveBranch, deleteFile });
|
|
126
|
+
return { ok: false, error: `foundry_git_finish: failed to write commit message file. ${err.message ?? String(err)}` };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Create the signed commit; roll back on failure. */
|
|
131
|
+
function doCommit(execGit, commitFile, branchName, archiveBranch, deleteFile) {
|
|
132
|
+
const res = safeGit(execGit, ['commit', '-S', '-F', commitFile]);
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
rollback({ execGit, branchName, archiveBranch, commitFile, deleteFile });
|
|
135
|
+
return { ok: false, error: gitError('foundry_git_finish: commit failed.', res.error) };
|
|
136
|
+
}
|
|
137
|
+
return { ok: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Delete the branch; return warning if it fails. */
|
|
141
|
+
function deleteBranch(execGit, branchName) {
|
|
142
|
+
const res = safeGit(execGit, ['branch', '-D', branchName]);
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
const msg = extractStderr(res.error);
|
|
145
|
+
return { ok: true, warning: `Branch '${branchName}' could not be deleted. ${msg}`.trim() };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Build the success result object. */
|
|
151
|
+
function successResult(hash, archiveBranch, tipSha, baseBranch, warning) {
|
|
152
|
+
const base = { ok: true, hash, archiveBranch, archiveTipSha: tipSha, branch: baseBranch };
|
|
153
|
+
return warning ? { ...base, warning } : base;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Run checkout + squash-merge steps. Returns error result on failure. */
|
|
157
|
+
function runMerge(execGit, branchName, baseBranch, archiveBranch, deleteFile) {
|
|
158
|
+
const checkout = checkoutBase(execGit, baseBranch, branchName, archiveBranch, deleteFile);
|
|
159
|
+
if (!checkout.ok) return checkout;
|
|
160
|
+
|
|
161
|
+
const merged = squashMerge(execGit, branchName, branchName, archiveBranch, deleteFile);
|
|
162
|
+
if (!merged.ok) return merged;
|
|
163
|
+
|
|
164
|
+
return { ok: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Run write-commit-message + commit steps. Returns error result on failure. */
|
|
168
|
+
function runCommit({ writeTempMessage, attestContent, execGit, branchName, archiveBranch, deleteFile }) {
|
|
169
|
+
const msg = writeCommitMsg({ writeTempMessage, attestContent, execGit, branchName, archiveBranch, deleteFile });
|
|
170
|
+
if (!msg.ok) return msg;
|
|
171
|
+
|
|
172
|
+
const committed = doCommit(execGit, msg.commitFile, branchName, archiveBranch, deleteFile);
|
|
173
|
+
if (!committed.ok) return committed;
|
|
174
|
+
|
|
175
|
+
return { ok: true, commitFile: msg.commitFile };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Run post-attestation steps: archive, merge, commit, cleanup, delete branch. */
|
|
179
|
+
function runPostAttestation({
|
|
180
|
+
execGit, branchName, baseBranch, deleteFile, writeTempMessage, shaCheck,
|
|
181
|
+
}) {
|
|
182
|
+
const tipSha = execGit(['rev-parse', branchName]).trim();
|
|
183
|
+
const archiveBranch = `archive/${branchName}-${tipSha.slice(0, 7)}`;
|
|
184
|
+
execGit(['branch', archiveBranch, branchName]);
|
|
185
|
+
|
|
186
|
+
const mergeResult = runMerge(execGit, branchName, baseBranch, archiveBranch, deleteFile);
|
|
187
|
+
if (!mergeResult.ok) return mergeResult;
|
|
188
|
+
|
|
189
|
+
const commitResult = runCommit({
|
|
190
|
+
writeTempMessage,
|
|
191
|
+
attestContent: shaCheck.attestContent,
|
|
192
|
+
execGit, branchName, archiveBranch, deleteFile,
|
|
193
|
+
});
|
|
194
|
+
if (!commitResult.ok) return commitResult;
|
|
195
|
+
|
|
196
|
+
if (deleteFile && commitResult.commitFile) {
|
|
197
|
+
bestEffort(() => deleteFile(commitResult.commitFile), []);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const hash = execGit(['rev-parse', '--short', 'HEAD']).trim();
|
|
201
|
+
const del = deleteBranch(execGit, branchName);
|
|
202
|
+
|
|
203
|
+
return successResult(hash, archiveBranch, tipSha, baseBranch, del.warning);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function finishWorkBranchWithArchive({
|
|
207
|
+
branchName,
|
|
208
|
+
baseBranch,
|
|
209
|
+
confirm,
|
|
210
|
+
execGit,
|
|
211
|
+
fileExists,
|
|
212
|
+
readAttest,
|
|
213
|
+
deleteFile,
|
|
214
|
+
writeTempMessage,
|
|
215
|
+
cwd,
|
|
216
|
+
}) {
|
|
217
|
+
if (confirm !== true) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
error: 'foundry_git_finish requires {confirm: true} to perform destructive operations.',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const fileCheck = checkAttestFile(cwd, fileExists, execGit);
|
|
225
|
+
if (!fileCheck.ok) return fileCheck;
|
|
226
|
+
|
|
227
|
+
const shaCheck = checkDiffSha(fileCheck.attestPath, readAttest, execGit, baseBranch);
|
|
228
|
+
if (!shaCheck.ok) return shaCheck;
|
|
229
|
+
|
|
230
|
+
return runPostAttestation({
|
|
231
|
+
execGit, branchName, baseBranch, deleteFile, writeTempMessage, shaCheck,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// scripts/lib/git-policy.js
|
|
2
|
+
//
|
|
3
|
+
// Shared git-commit policy used by the orchestrator's git bridge.
|
|
4
|
+
//
|
|
5
|
+
// The orchestrator commits twice per cycle: a setup commit that configures
|
|
6
|
+
// stages in WORK.md and a post-finalise commit after each stage finalise.
|
|
7
|
+
// This module defines the phase-specific file allow-lists for those commits
|
|
8
|
+
// and reports a structured violation when the worktree contains any other
|
|
9
|
+
// paths.
|
|
10
|
+
//
|
|
11
|
+
// This module accepts raw inputs (porcelain string, allowed pattern list)
|
|
12
|
+
// and returns plain data. The bridge wires these helpers to git plumbing.
|
|
13
|
+
|
|
14
|
+
import { minimatch } from 'minimatch';
|
|
15
|
+
|
|
16
|
+
export const TOOL_MANAGED = [
|
|
17
|
+
'WORK.md',
|
|
18
|
+
'WORK.history.yaml',
|
|
19
|
+
'WORK.feedback.yaml',
|
|
20
|
+
// The plugin's secret bootstrap idempotently appends `.foundry/` to the
|
|
21
|
+
// project's `.gitignore` (see scripts/lib/secret.js). Treat it as
|
|
22
|
+
// tool-managed so the orchestrator's setup commit can sweep up that change
|
|
23
|
+
// without flagging it as an unexpected dirty file.
|
|
24
|
+
'.gitignore',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const TOOL_MANAGED_PREFIX = ['.foundry/'];
|
|
28
|
+
|
|
29
|
+
export function isToolManaged(file) {
|
|
30
|
+
if (TOOL_MANAGED.includes(file)) return true;
|
|
31
|
+
return TOOL_MANAGED_PREFIX.some((p) => file.startsWith(p));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function addIfNew(seen, result, path) {
|
|
35
|
+
if (path && !seen.has(path)) {
|
|
36
|
+
seen.add(path);
|
|
37
|
+
result.push(path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isRenameOrCopy(status) {
|
|
42
|
+
return status[0] === 'R' || status[0] === 'C';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse `git status -z --porcelain=v1` output into a list of file paths.
|
|
47
|
+
*
|
|
48
|
+
* Each record is null-terminated. Renames/copies (R/C) are emitted as two
|
|
49
|
+
* null-terminated paths: `R new\0old\0` (i.e. the destination first, then
|
|
50
|
+
* the source). Per git-status(1), the -z format reverses the usual "from -> to"
|
|
51
|
+
* order to "to from" for renames. We surface BOTH paths so the caller can refuse
|
|
52
|
+
* a commit that would carry along an unrelated source path.
|
|
53
|
+
*
|
|
54
|
+
* Returns a de-duplicated array of paths in encounter order.
|
|
55
|
+
*/
|
|
56
|
+
export function parsePorcelainZ(out) {
|
|
57
|
+
if (!out) return [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const result = [];
|
|
60
|
+
const parts = out.split('\0');
|
|
61
|
+
for (let i = 0; i < parts.length; i++) {
|
|
62
|
+
const entry = parts[i];
|
|
63
|
+
if (!entry) continue;
|
|
64
|
+
const path = entry.slice(3);
|
|
65
|
+
addIfNew(seen, result, path);
|
|
66
|
+
if (isRenameOrCopy(entry)) {
|
|
67
|
+
addIfNew(seen, result, parts[++i]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Partition a list of dirty files into (allowed, unexpected) given the
|
|
75
|
+
* stage's allowed glob patterns. Tool-managed files are ALWAYS allowed.
|
|
76
|
+
*/
|
|
77
|
+
export function partitionDirty(files, allowedPatterns = []) {
|
|
78
|
+
const allowed = [];
|
|
79
|
+
const unexpected = [];
|
|
80
|
+
for (const f of files) {
|
|
81
|
+
if (isToolManaged(f)) { allowed.push(f); continue; }
|
|
82
|
+
if (allowedPatterns.some((p) => minimatch(f, p, { dot: true }))) { allowed.push(f); continue; }
|
|
83
|
+
unexpected.push(f);
|
|
84
|
+
}
|
|
85
|
+
return { allowed, unexpected };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compute the allowed glob patterns for a given stage.
|
|
90
|
+
*
|
|
91
|
+
* - forge: artefact type's file-patterns (caller resolves; passed in).
|
|
92
|
+
* - assay: writes under foundry-memory/**.
|
|
93
|
+
* - quench / appraise / human-appraise: no surface files allowed; only
|
|
94
|
+
* tool-managed files may change.
|
|
95
|
+
* - setup (no stage): no surface files allowed.
|
|
96
|
+
*/
|
|
97
|
+
export function allowedPatternsForStage({ stageBase, forgeFilePatterns = [] } = {}) {
|
|
98
|
+
if (stageBase === 'forge') return forgeFilePatterns;
|
|
99
|
+
if (stageBase === 'assay') return ['foundry-memory/**'];
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { requireNotFailed } from './failed-flow.js';
|
|
2
|
+
import { appendTraceRecord } from './tracing.js';
|
|
3
|
+
import { DRY_RUN_RE } from './branch-guard.js';
|
|
4
|
+
|
|
5
|
+
const MAX_ARG_STR = 4096;
|
|
6
|
+
const HEAD_LEN = 256;
|
|
7
|
+
const TAIL_LEN = 256;
|
|
8
|
+
|
|
9
|
+
function truncateString(v) {
|
|
10
|
+
const elided = v.length - HEAD_LEN - TAIL_LEN;
|
|
11
|
+
return v.slice(0, HEAD_LEN)
|
|
12
|
+
+ `...(${elided} chars elided)...`
|
|
13
|
+
+ v.slice(v.length - TAIL_LEN);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function scrubValue(v) {
|
|
17
|
+
if (typeof v === 'string' && v.length > MAX_ARG_STR) {
|
|
18
|
+
return truncateString(v);
|
|
19
|
+
}
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scrub(args) {
|
|
24
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) return args;
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [k, v] of Object.entries(args)) {
|
|
27
|
+
out[k] = scrubValue(v);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseResultMaybeJson(s) {
|
|
33
|
+
if (typeof s !== 'string') return s;
|
|
34
|
+
try { return JSON.parse(s); } catch { return s; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveBranch(branchIo, context) {
|
|
38
|
+
try {
|
|
39
|
+
const out = branchIo(context).exec(['git', 'rev-parse', '--abbrev-ref', 'HEAD']);
|
|
40
|
+
const branch = String(out ?? '').trim();
|
|
41
|
+
if (!branch || branch === 'HEAD') return null;
|
|
42
|
+
return branch;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shouldTraceBranch(branch, io) {
|
|
49
|
+
return branch !== null && DRY_RUN_RE.test(branch) && typeof io === 'function';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runGuards(guards, args, context, toolName) {
|
|
53
|
+
for (const g of guards) {
|
|
54
|
+
const r = await g(args, context);
|
|
55
|
+
if (!r.ok) {
|
|
56
|
+
return JSON.stringify({ error: `${toolName}: ${r.error}` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildTraceRecord(opts) {
|
|
63
|
+
const { ts, toolName, args, result, error, durationMs } = opts;
|
|
64
|
+
const base = { ts, tool: toolName, args: scrub(args), duration_ms: durationMs };
|
|
65
|
+
if (error) {
|
|
66
|
+
return { ...base, error: error.message ?? String(error) };
|
|
67
|
+
}
|
|
68
|
+
return { ...base, result: parseResultMaybeJson(result) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function writeTraceRecord(opts) {
|
|
72
|
+
const { branch, record, io } = opts;
|
|
73
|
+
try {
|
|
74
|
+
await appendTraceRecord({ branch, record, io });
|
|
75
|
+
} catch (traceErr) {
|
|
76
|
+
if (process.env.FOUNDRY_DEBUG) {
|
|
77
|
+
console.warn(
|
|
78
|
+
`[foundry] ${opts.toolName}: trace write failed (${traceErr.message ?? String(traceErr)})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function executeAndTrace(opts) {
|
|
85
|
+
const { branchIo, io, execute, args, context, toolName } = opts;
|
|
86
|
+
const ts = new Date().toISOString();
|
|
87
|
+
const t0 = Date.now();
|
|
88
|
+
const branch = resolveBranch(branchIo, context);
|
|
89
|
+
const trace = shouldTraceBranch(branch, io);
|
|
90
|
+
|
|
91
|
+
let result;
|
|
92
|
+
let error;
|
|
93
|
+
try {
|
|
94
|
+
result = await execute(args, context);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
error = e;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (trace) {
|
|
100
|
+
const record = buildTraceRecord({ ts, toolName, args, result, error, durationMs: Date.now() - t0 });
|
|
101
|
+
await writeTraceRecord({ branch, record, io: io(context), toolName });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (error) throw error;
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function guarded(toolName, guards, execute, opts = {}) {
|
|
109
|
+
return async (args, context) => {
|
|
110
|
+
const guardError = await runGuards(guards, args, context, toolName);
|
|
111
|
+
if (guardError) return guardError;
|
|
112
|
+
|
|
113
|
+
if (!opts.branchIo) {
|
|
114
|
+
return execute(args, context);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return executeAndTrace({ ...opts, execute, args, context, toolName });
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `makeIO` is provided by the caller, since failed-flow lives in a
|
|
122
|
+
// different layer than the plugin helpers. We accept an io factory.
|
|
123
|
+
export function notFailedGuard(makeSyncIO) {
|
|
124
|
+
return (_args, context) => requireNotFailed(makeSyncIO(context.worktree));
|
|
125
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
import { markWorkfileFailed } from './failed-flow.js';
|
|
3
|
+
import { sortPaths } from './attestation/hash.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse WORK.history.yaml text into an array of entries.
|
|
7
|
+
* Throws `WORK.history.yaml malformed: ...` on parse failure or non-array root.
|
|
8
|
+
* Empty/null/undefined input yields [].
|
|
9
|
+
*/
|
|
10
|
+
function parseHistory(text) {
|
|
11
|
+
let data;
|
|
12
|
+
try {
|
|
13
|
+
data = yaml.load(text) || [];
|
|
14
|
+
if (!Array.isArray(data)) {
|
|
15
|
+
throw new Error('root is not an array');
|
|
16
|
+
}
|
|
17
|
+
} catch (err) {
|
|
18
|
+
throw new Error(`WORK.history.yaml malformed: ${err.message}`, { cause: err });
|
|
19
|
+
}
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse the full WORK.history.yaml text into all entries (no cycle filter).
|
|
25
|
+
* Returns entries in file declaration order. Throws on malformed YAML or non-array root.
|
|
26
|
+
* Empty/null/undefined input yields [].
|
|
27
|
+
*/
|
|
28
|
+
export function parseAllHistoryEntries(text) {
|
|
29
|
+
return parseHistory(text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function markFailedDefensive(io, msg) {
|
|
33
|
+
try { markWorkfileFailed(io, msg); } catch { /* WORK.md gone; nothing to mark */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function entryTimestamp(entry) {
|
|
37
|
+
return entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function entrySeq(entry) {
|
|
41
|
+
return typeof entry.seq === 'number' ? entry.seq : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compareEntries(a, b) {
|
|
45
|
+
const ta = entryTimestamp(a);
|
|
46
|
+
const tb = entryTimestamp(b);
|
|
47
|
+
if (ta !== tb) return ta - tb;
|
|
48
|
+
return entrySeq(a) - entrySeq(b);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load history entries for a cycle, sorted by timestamp ascending.
|
|
53
|
+
*/
|
|
54
|
+
export function loadHistory(historyPath, cycle, io) {
|
|
55
|
+
if (!io.exists(historyPath)) return [];
|
|
56
|
+
let data;
|
|
57
|
+
try {
|
|
58
|
+
data = parseHistory(io.readFile(historyPath));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
markFailedDefensive(io, err.message);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
const filtered = data.filter(e => e.cycle === cycle);
|
|
64
|
+
filtered.sort(compareEntries);
|
|
65
|
+
return filtered;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requireIteration(value) {
|
|
69
|
+
if (value === undefined) throw new Error('iteration is required');
|
|
70
|
+
if (value === null) throw new Error('iteration is required');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateAppendArgs({ iteration, comment, route, stage }) {
|
|
74
|
+
requireIteration(iteration);
|
|
75
|
+
if (!comment) throw new Error('comment is required');
|
|
76
|
+
if (route !== undefined && stage !== 'sort') {
|
|
77
|
+
throw new Error(`route is only valid on stage='sort' entries; got stage='${stage}'`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildEntry({ cycle, stage, iteration, comment, route, openFeedback, changedFiles }, seq) {
|
|
82
|
+
const entry = {
|
|
83
|
+
cycle,
|
|
84
|
+
stage,
|
|
85
|
+
iteration,
|
|
86
|
+
comment,
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
seq,
|
|
89
|
+
open_feedback: openFeedback ?? 0,
|
|
90
|
+
};
|
|
91
|
+
if (route !== undefined) entry.route = route;
|
|
92
|
+
if (changedFiles !== undefined) {
|
|
93
|
+
entry.changed_files = sortPaths(changedFiles);
|
|
94
|
+
}
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Append a history entry with auto-generated ISO timestamp.
|
|
100
|
+
*/
|
|
101
|
+
export function appendEntry(historyPath, entryOpts, io) {
|
|
102
|
+
validateAppendArgs(entryOpts);
|
|
103
|
+
|
|
104
|
+
let existing = [];
|
|
105
|
+
if (io.exists(historyPath)) {
|
|
106
|
+
try {
|
|
107
|
+
existing = parseHistory(io.readFile(historyPath));
|
|
108
|
+
} catch (err) {
|
|
109
|
+
markFailedDefensive(io, err.message);
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const entry = buildEntry(entryOpts, existing.length);
|
|
115
|
+
existing.push(entry);
|
|
116
|
+
|
|
117
|
+
const body = yaml.dump(existing);
|
|
118
|
+
const tmp = `${historyPath}.tmp`;
|
|
119
|
+
io.writeFile(tmp, body);
|
|
120
|
+
io.rename(tmp, historyPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Count COMPLETED forge stages for a cycle. This includes forges that ran to
|
|
125
|
+
* completion but whose downstream appraise deadlocked or blocked the cycle —
|
|
126
|
+
* completion here means "stage_end was called", not "cycle progressed".
|
|
127
|
+
* Used by sort for max-iterations enforcement.
|
|
128
|
+
*/
|
|
129
|
+
export function getIteration(historyPath, cycle, io) {
|
|
130
|
+
const history = loadHistory(historyPath, cycle, io);
|
|
131
|
+
return history.filter(e => (e.stage || '').split(':')[0] === 'forge').length;
|
|
132
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { memoryPaths } from '../paths.js';
|
|
2
|
+
import { loadSchema, writeSchema, bumpVersion, hashFrontmatter } from '../schema.js';
|
|
3
|
+
import { invalidateStore } from '../singleton.js';
|
|
4
|
+
import { withLiveMemoryDb, createLiveEdgeType } from './live-store.js';
|
|
5
|
+
|
|
6
|
+
const IDENT = /^[a-z][a-z0-9_]*$/;
|
|
7
|
+
|
|
8
|
+
function deduplicateFirstOccurrence(list) {
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const name of list) {
|
|
12
|
+
if (seen.has(name)) continue;
|
|
13
|
+
seen.add(name);
|
|
14
|
+
out.push(name);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normaliseList(v, key) {
|
|
20
|
+
if (v === 'any') return 'any';
|
|
21
|
+
if (!Array.isArray(v) || v.length === 0 || !v.every((s) => typeof s === 'string' && s)) {
|
|
22
|
+
throw new Error(`'${key}' must be 'any' or a non-empty list of entity type names`);
|
|
23
|
+
}
|
|
24
|
+
// Deduplicate while preserving first-occurrence order. `[class, class]` is a
|
|
25
|
+
// user mistake, not a meaningful declaration.
|
|
26
|
+
return deduplicateFirstOccurrence(v);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderFrontmatter(fm) {
|
|
30
|
+
const lines = [`type: ${fm.type}`];
|
|
31
|
+
for (const key of ['sources', 'targets']) {
|
|
32
|
+
const v = fm[key];
|
|
33
|
+
lines.push(v === 'any' ? `${key}: any` : `${key}: [${v.join(', ')}]`);
|
|
34
|
+
}
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateEdgeTypeInputs(name, body) {
|
|
39
|
+
if (!IDENT.test(name)) throw new Error(`invalid identifier: '${name}'`);
|
|
40
|
+
if (typeof body !== 'string' || !body.trim()) throw new Error(`body must be non-empty`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function assertNoDuplicateDeclaration(name, schema, p, io) {
|
|
44
|
+
if (schema.edges[name]) throw new Error(`edge type '${name}' already exists`);
|
|
45
|
+
if (schema.entities[name]) throw new Error(`'${name}' is already declared as an entity type`);
|
|
46
|
+
if (await io.exists(p.edgeTypeFile(name))) throw new Error(`edge type file already exists on disk`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertEntityTypesExist(lists, entities) {
|
|
50
|
+
for (const list of lists) {
|
|
51
|
+
if (list === 'any') continue;
|
|
52
|
+
for (const t of list) {
|
|
53
|
+
if (!entities[t]) throw new Error(`entity type '${t}' is not declared`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function validateEdgeTypeCreation({ name, srcs, tgts, schema, p, io }) {
|
|
59
|
+
await assertNoDuplicateDeclaration(name, schema, p, io);
|
|
60
|
+
assertEntityTypesExist([srcs, tgts], schema.entities);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function createEdgeType({ worktreeRoot, io, name, sources, targets, body }) {
|
|
64
|
+
validateEdgeTypeInputs(name, body);
|
|
65
|
+
const srcs = normaliseList(sources, 'sources');
|
|
66
|
+
const tgts = normaliseList(targets, 'targets');
|
|
67
|
+
|
|
68
|
+
const foundryDir = 'foundry';
|
|
69
|
+
const p = memoryPaths(foundryDir);
|
|
70
|
+
const schema = await loadSchema(foundryDir, io);
|
|
71
|
+
|
|
72
|
+
await validateEdgeTypeCreation({ name, body, srcs, tgts, schema, p, io });
|
|
73
|
+
|
|
74
|
+
const frontmatter = { type: name, sources: srcs, targets: tgts };
|
|
75
|
+
const fileContent = `---\n${renderFrontmatter(frontmatter)}\n---\n\n${body.trim()}\n`;
|
|
76
|
+
await io.writeFile(p.edgeTypeFile(name), fileContent);
|
|
77
|
+
await io.writeFile(p.relationFile(name), '');
|
|
78
|
+
|
|
79
|
+
schema.edges[name] = { frontmatterHash: hashFrontmatter(frontmatter) };
|
|
80
|
+
bumpVersion(schema);
|
|
81
|
+
await writeSchema(foundryDir, schema, io);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await withLiveMemoryDb({ worktreeRoot, io }, async (db) => {
|
|
85
|
+
await createLiveEdgeType(db, name);
|
|
86
|
+
});
|
|
87
|
+
} finally {
|
|
88
|
+
invalidateStore(worktreeRoot);
|
|
89
|
+
}
|
|
90
|
+
return { type: name, sources: srcs, targets: tgts };
|
|
91
|
+
}
|