@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,354 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
|
|
4
|
+
import { slugify } from '../../../scripts/lib/slug.js';
|
|
5
|
+
import { CONFIG_RE, DRY_RUN_RE } from '../../../scripts/lib/branch-guard.js';
|
|
6
|
+
import { finishWorkBranchWithArchive } from '../../../scripts/lib/git-finish/work-finish.js';
|
|
7
|
+
import { finishDryRun } from '../../../scripts/lib/snapshot/finish.js';
|
|
8
|
+
import { asyncIoFactory } from './helpers.js';
|
|
9
|
+
|
|
10
|
+
const WORK_FILES = ['WORK.md', 'WORK.history.yaml', 'WORK.feedback.yaml'];
|
|
11
|
+
|
|
12
|
+
export const KIND_CONFIG = 'config';
|
|
13
|
+
export const KIND_WORK = 'work';
|
|
14
|
+
export const KIND_DRY_RUN = 'dry-run';
|
|
15
|
+
export const KINDS = [KIND_CONFIG, KIND_WORK, KIND_DRY_RUN];
|
|
16
|
+
|
|
17
|
+
const WORK_RE = /^work\/.+$/;
|
|
18
|
+
const DRY_RUN_DEEPER_RE = /^dry-run\/[^/]+\/[^/]+\/.+$/;
|
|
19
|
+
|
|
20
|
+
// -- Shared: stderr extraction --
|
|
21
|
+
|
|
22
|
+
function extractStderr(err) {
|
|
23
|
+
if (!err) return '';
|
|
24
|
+
const text = err.stderr || err.stdout;
|
|
25
|
+
return text ? String(text).trim() : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// -- Kind argument validation --
|
|
29
|
+
|
|
30
|
+
function validateConfigArgs(args) {
|
|
31
|
+
if (args.flowId !== undefined && args.flowId !== null && args.flowId !== '')
|
|
32
|
+
return `flowId is not valid for kind="config"; supply only { kind, description }.`;
|
|
33
|
+
if (!args.description)
|
|
34
|
+
return `description is required for kind="config".`;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateFlowArgs(kind, args) {
|
|
39
|
+
if (!args.flowId)
|
|
40
|
+
return `flowId is required for kind="${kind}".`;
|
|
41
|
+
if (!args.description)
|
|
42
|
+
return `description is required for kind="${kind}".`;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateKindArgs(kind, args) {
|
|
47
|
+
if (kind === KIND_CONFIG) return validateConfigArgs(args);
|
|
48
|
+
if (kind === KIND_WORK || kind === KIND_DRY_RUN) return validateFlowArgs(kind, args);
|
|
49
|
+
return `unknown kind "${kind}"; expected one of: ${KINDS.join(', ')}.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// -- Starting branch validation --
|
|
53
|
+
|
|
54
|
+
function validateDryRunDepth(branch) {
|
|
55
|
+
if (branch && (DRY_RUN_RE.test(branch) || DRY_RUN_DEEPER_RE.test(branch)))
|
|
56
|
+
return `cannot nest deeper than one dry-run level; you are on '${branch}'.`;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateConfigStartBranch(branch) {
|
|
61
|
+
if (branch && CONFIG_RE.test(branch))
|
|
62
|
+
return `already on a config/* branch ('${branch}'); edit here directly or finish first.`;
|
|
63
|
+
if (branch && WORK_RE.test(branch))
|
|
64
|
+
return `cannot start a config branch from a work branch ('${branch}'); finish or abandon it first.`;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateWorkStartBranch(branch) {
|
|
69
|
+
if (branch && CONFIG_RE.test(branch))
|
|
70
|
+
return `cannot start a work branch from a config branch ('${branch}'); ` +
|
|
71
|
+
`use kind="dry-run" to dry-run the in-progress config, or finish config first.`;
|
|
72
|
+
if (branch && WORK_RE.test(branch))
|
|
73
|
+
return `already on a work branch ('${branch}'); finish or abandon it first.`;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateDryRunStartBranch(branch) {
|
|
78
|
+
if (!branch || !CONFIG_RE.test(branch))
|
|
79
|
+
return `kind="dry-run" requires a config/<description> branch as starting point; ` +
|
|
80
|
+
`currently on ${branch ? "'" + branch + "'" : 'detached HEAD'}.`;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validateStartingBranch(kind, branch) {
|
|
85
|
+
const depthErr = validateDryRunDepth(branch);
|
|
86
|
+
if (depthErr) return depthErr;
|
|
87
|
+
if (kind === KIND_CONFIG) return validateConfigStartBranch(branch);
|
|
88
|
+
if (kind === KIND_WORK) return validateWorkStartBranch(branch);
|
|
89
|
+
if (kind === KIND_DRY_RUN) return validateDryRunStartBranch(branch);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// -- Branch name construction --
|
|
94
|
+
|
|
95
|
+
function buildConfigBranchName(descSlug) {
|
|
96
|
+
return { name: `config/${descSlug}` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildWorkBranchName(flowSlug, descSlug) {
|
|
100
|
+
return { name: `work/${flowSlug}-${descSlug}` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildDryRunBranchName(parentBranch, flowSlug, descSlug) {
|
|
104
|
+
const parentSlug = parentBranch.replace(/^config\//, '');
|
|
105
|
+
return { name: `dry-run/${parentSlug}/${flowSlug}-${descSlug}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requireFlowSlug(args) {
|
|
109
|
+
const flowSlug = slugify(args.flowId);
|
|
110
|
+
if (!flowSlug) return { error: 'flowId slug is empty after normalisation.' };
|
|
111
|
+
return { flowSlug };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildFlowBranch(kind, parentBranch, flowSlug, descSlug) {
|
|
115
|
+
if (kind === KIND_WORK) return buildWorkBranchName(flowSlug, descSlug);
|
|
116
|
+
if (kind === KIND_DRY_RUN) return buildDryRunBranchName(parentBranch, flowSlug, descSlug);
|
|
117
|
+
return { error: 'internal: unhandled kind' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildBranchName(kind, args, parentBranch) {
|
|
121
|
+
const descSlug = slugify(args.description);
|
|
122
|
+
if (!descSlug) return { error: 'description slug is empty after normalisation.' };
|
|
123
|
+
if (kind === KIND_CONFIG) return buildConfigBranchName(descSlug);
|
|
124
|
+
const flowResult = requireFlowSlug(args);
|
|
125
|
+
if (flowResult.error) return flowResult;
|
|
126
|
+
return buildFlowBranch(kind, parentBranch, flowResult.flowSlug, descSlug);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// -- Branch classification --
|
|
130
|
+
|
|
131
|
+
export function classifyBranch(branch) {
|
|
132
|
+
if (!branch) return 'detached';
|
|
133
|
+
if (DRY_RUN_RE.test(branch)) return 'dry-run';
|
|
134
|
+
if (CONFIG_RE.test(branch)) return 'config';
|
|
135
|
+
if (WORK_RE.test(branch)) return 'work';
|
|
136
|
+
return 'other';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// -- Dirty-file detection --
|
|
140
|
+
|
|
141
|
+
export function dirtyTrackedFiles(cwd) {
|
|
142
|
+
const out = execFileSync('git',
|
|
143
|
+
['status', '--porcelain', '--untracked-files=no'],
|
|
144
|
+
{ cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
145
|
+
return out ? out.split('\n').map((l) => l.slice(3)) : [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// -- finishBranchCommon helpers --
|
|
149
|
+
|
|
150
|
+
function computeFinishPlan(opts) {
|
|
151
|
+
const { branchName, branchType, base, args, cwd } = opts;
|
|
152
|
+
const shouldDelete = branchType === 'work';
|
|
153
|
+
const filesToDelete = shouldDelete
|
|
154
|
+
? WORK_FILES.filter((f) => existsSync(path.join(cwd, f)))
|
|
155
|
+
: [];
|
|
156
|
+
const action = shouldDelete
|
|
157
|
+
? 'delete-work-files, commit-cleanup, checkout-base, squash-merge, commit, delete-work-branch'
|
|
158
|
+
: `checkout-base, squash-merge, commit, delete-${branchType}-branch`;
|
|
159
|
+
return { workBranch: branchName, baseBranch: base, filesToDelete, action, commitMessage: args.message };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function makeConfirmRefusal(planned) {
|
|
163
|
+
return JSON.stringify({
|
|
164
|
+
ok: false,
|
|
165
|
+
error: 'foundry_git_finish requires {confirm: true} to perform destructive operations. Re-invoke with confirm:true to apply the plan.',
|
|
166
|
+
planned,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function makeDirtyRefusal(dirty) {
|
|
171
|
+
return JSON.stringify({
|
|
172
|
+
ok: false,
|
|
173
|
+
error: 'foundry_git_finish refuses to run on a dirty worktree (uncommitted changes to tracked files). Commit or stash them first.',
|
|
174
|
+
dirty,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function mergeErrorLabel(branchType) {
|
|
179
|
+
return branchType === 'work' ? 'Work' : 'Config';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatMergeError(branchName, branchType, err) {
|
|
183
|
+
const branchLabel = mergeErrorLabel(branchType);
|
|
184
|
+
const stderr = extractStderr(err);
|
|
185
|
+
const suffix = stderr ? ' ' + stderr : '';
|
|
186
|
+
return `foundry_git_finish: squash merge failed (likely a conflict). ${branchLabel} branch '${branchName}' preserved.${suffix}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bestEffortReset(opts) {
|
|
190
|
+
try { execFileSync('git', ['reset', '--hard', 'HEAD'], opts); } catch { /* best-effort */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function bestEffortCheckout(branchName, opts) {
|
|
194
|
+
try { execFileSync('git', ['checkout', branchName], opts); } catch { /* best-effort */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function squashMergeIntoBase(base, branchName, branchType, opts) {
|
|
198
|
+
execFileSync('git', ['checkout', base], opts);
|
|
199
|
+
try {
|
|
200
|
+
execFileSync('git', ['merge', '--squash', branchName], opts);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
bestEffortReset(opts);
|
|
203
|
+
bestEffortCheckout(branchName, opts);
|
|
204
|
+
return JSON.stringify({ ok: false, error: formatMergeError(branchName, branchType, err) });
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function commitAndDeleteBranch(message, branchName, opts) {
|
|
210
|
+
execFileSync('git', ['commit', '--no-gpg-sign', '-m', message], opts);
|
|
211
|
+
const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], opts).trim();
|
|
212
|
+
execFileSync('git', ['branch', '-D', branchName], opts);
|
|
213
|
+
return { hash };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function deleteWorkFilesAndCommit(filesToDelete, cwd, branchName) {
|
|
217
|
+
for (const f of filesToDelete) {
|
|
218
|
+
const p = path.join(cwd, f);
|
|
219
|
+
if (existsSync(p)) unlinkSync(p);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const opts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
223
|
+
execFileSync('git', ['add', '-A'], opts);
|
|
224
|
+
const status = execFileSync('git', ['status', '--porcelain'], opts).trim();
|
|
225
|
+
if (status) {
|
|
226
|
+
const cleanupMsg = `[${branchName.replace('work/', '')}] cleanup: remove work files`;
|
|
227
|
+
execFileSync('git', ['commit', '-m', cleanupMsg], opts);
|
|
228
|
+
}
|
|
229
|
+
} catch { /* no changes to commit */ }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function finishBranchCommon({ branchName, branchType, base, cwd, args }) {
|
|
233
|
+
const opts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
234
|
+
const planned = computeFinishPlan({ branchName, branchType, base, args, cwd });
|
|
235
|
+
if (args.confirm !== true) return makeConfirmRefusal(planned);
|
|
236
|
+
const dirty = dirtyTrackedFiles(cwd);
|
|
237
|
+
if (dirty.length) return makeDirtyRefusal(dirty);
|
|
238
|
+
if (branchType === 'work') deleteWorkFilesAndCommit(planned.filesToDelete, cwd, branchName);
|
|
239
|
+
const mergeErr = squashMergeIntoBase(base, branchName, branchType, opts);
|
|
240
|
+
if (mergeErr) return mergeErr;
|
|
241
|
+
const { hash } = commitAndDeleteBranch(args.message, branchName, opts);
|
|
242
|
+
return JSON.stringify({ ok: true, hash, branch: base });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// -- finishWorkBranch helpers --
|
|
246
|
+
|
|
247
|
+
function makeExecGit(cwd, opts) {
|
|
248
|
+
return (argv) => {
|
|
249
|
+
if (argv[0] === 'diff')
|
|
250
|
+
return execFileSync('git', argv, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
251
|
+
return execFileSync('git', argv, opts);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function makeWriteTempMessage(cwd, opts) {
|
|
256
|
+
return (content) => {
|
|
257
|
+
const gitDir = execFileSync('git', ['rev-parse', '--git-dir'], opts).trim();
|
|
258
|
+
const gitDirAbsolute = path.isAbsolute(gitDir) ? gitDir : path.join(cwd, gitDir);
|
|
259
|
+
const tmpPath = path.join(gitDirAbsolute, `COMMIT_EDITMSG_${Date.now()}`);
|
|
260
|
+
writeFileSync(tmpPath, content, 'utf8');
|
|
261
|
+
return tmpPath;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function makeWorkFinishPlanned(workBranch, base) {
|
|
266
|
+
return {
|
|
267
|
+
workBranch,
|
|
268
|
+
baseBranch: base,
|
|
269
|
+
action: 'verify-attest, checkout-base, squash-merge, signed-commit',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatWorkFinishError(err) {
|
|
274
|
+
const stderr = extractStderr(err);
|
|
275
|
+
if (stderr) return `foundry_git_finish: attested work finish failed. ${stderr}`;
|
|
276
|
+
return `foundry_git_finish: attested work finish failed. ${err.message ?? String(err)}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function finishWorkBranch({ workBranch, base, cwd, args }) {
|
|
280
|
+
const opts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
281
|
+
if (args.confirm !== true) {
|
|
282
|
+
return JSON.stringify({
|
|
283
|
+
ok: false,
|
|
284
|
+
error: 'foundry_git_finish requires {confirm: true}.',
|
|
285
|
+
planned: makeWorkFinishPlanned(workBranch, base),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const dirty = dirtyTrackedFiles(cwd);
|
|
289
|
+
if (dirty.length) {
|
|
290
|
+
return JSON.stringify({ ok: false, error: 'foundry_git_finish: dirty tracked files.', dirty });
|
|
291
|
+
}
|
|
292
|
+
const execGit = makeExecGit(cwd, opts);
|
|
293
|
+
const writeTempMessage = makeWriteTempMessage(cwd, opts);
|
|
294
|
+
try {
|
|
295
|
+
const result = await finishWorkBranchWithArchive({
|
|
296
|
+
branchName: workBranch,
|
|
297
|
+
baseBranch: base,
|
|
298
|
+
confirm: args.confirm,
|
|
299
|
+
execGit,
|
|
300
|
+
fileExists: (p) => existsSync(p),
|
|
301
|
+
readAttest: (p) => readFileSync(p, 'utf8'),
|
|
302
|
+
deleteFile: (p) => { if (existsSync(p)) unlinkSync(p); },
|
|
303
|
+
writeTempMessage,
|
|
304
|
+
cwd,
|
|
305
|
+
});
|
|
306
|
+
return JSON.stringify(result);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return JSON.stringify({ ok: false, error: formatWorkFinishError(err) });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// -- finishConfigBranch --
|
|
313
|
+
|
|
314
|
+
export function finishConfigBranch({ configBranch, base, cwd, args }) {
|
|
315
|
+
return finishBranchCommon({
|
|
316
|
+
branchName: configBranch,
|
|
317
|
+
branchType: 'config',
|
|
318
|
+
base,
|
|
319
|
+
cwd,
|
|
320
|
+
args,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// -- finishDryRunBranch --
|
|
325
|
+
|
|
326
|
+
export async function finishDryRunBranch({ branch, args, cwd }) {
|
|
327
|
+
const io = asyncIoFactory({ worktree: cwd });
|
|
328
|
+
const exec = (argv) => execFileSync('git', argv,
|
|
329
|
+
{ cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
330
|
+
|
|
331
|
+
if (args.confirm !== true) {
|
|
332
|
+
return JSON.stringify({
|
|
333
|
+
ok: false,
|
|
334
|
+
error: 'foundry_git_finish requires {confirm: true} to perform destructive operations. Re-invoke with confirm:true to apply the plan.',
|
|
335
|
+
planned: {
|
|
336
|
+
branch,
|
|
337
|
+
action: 'snapshot + discard (dry-run finish)',
|
|
338
|
+
snapshotPath: '.snapshots/<runId> (computed at apply time)',
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const out = await finishDryRun({
|
|
345
|
+
message: args.message, branch, io, execFile: exec,
|
|
346
|
+
});
|
|
347
|
+
return JSON.stringify(out);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return JSON.stringify({
|
|
350
|
+
ok: false,
|
|
351
|
+
error: `foundry_git_finish: dry-run finish failed: ${err.message ?? String(err)}`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import {
|
|
3
|
+
validateKindArgs,
|
|
4
|
+
validateStartingBranch,
|
|
5
|
+
buildBranchName,
|
|
6
|
+
classifyBranch,
|
|
7
|
+
finishWorkBranch,
|
|
8
|
+
finishConfigBranch,
|
|
9
|
+
finishDryRunBranch,
|
|
10
|
+
KIND_DRY_RUN,
|
|
11
|
+
KINDS,
|
|
12
|
+
} from './git-helpers.js';
|
|
13
|
+
import { makeIO, makeExec, asyncIoFactory } from './helpers.js';
|
|
14
|
+
import { requireNoActiveStage } from '../../../scripts/lib/stage-guard.js';
|
|
15
|
+
import { currentBranch } from '../../../scripts/lib/branch-guard.js';
|
|
16
|
+
import { truncateTrace } from '../../../scripts/lib/tracing.js';
|
|
17
|
+
|
|
18
|
+
function refuse(error) { return JSON.stringify({ error }); }
|
|
19
|
+
|
|
20
|
+
// -- foundry_git_branch helpers --
|
|
21
|
+
|
|
22
|
+
function validateBranchCreationArgs(args) {
|
|
23
|
+
if (!args.kind)
|
|
24
|
+
return 'foundry_git_branch: kind is required (one of: config, work, dry-run)';
|
|
25
|
+
if (!KINDS.includes(args.kind))
|
|
26
|
+
return `foundry_git_branch: unknown kind "${args.kind}" ` +
|
|
27
|
+
`(expected one of: ${KINDS.join(', ')})`;
|
|
28
|
+
const argErr = validateKindArgs(args.kind, args);
|
|
29
|
+
if (argErr) return `foundry_git_branch: ${argErr}`;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function refuseBranchCreate(branchName, err) {
|
|
34
|
+
if (!err) return refuse(`foundry_git_branch: failed to create branch '${branchName}'.`);
|
|
35
|
+
const text = err.stderr || err.stdout;
|
|
36
|
+
if (!text) return refuse(`foundry_git_branch: failed to create branch '${branchName}'.`);
|
|
37
|
+
return refuse(`foundry_git_branch: failed to create branch '${branchName}'. ${String(text).trim()}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function tryCreateBranch(name, cwd) {
|
|
41
|
+
try {
|
|
42
|
+
execFileSync('git', ['checkout', '-b', name],
|
|
43
|
+
{ cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
44
|
+
return null;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return refuseBranchCreate(name, err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function tryTruncateTrace(branch, worktree) {
|
|
51
|
+
try {
|
|
52
|
+
await truncateTrace({
|
|
53
|
+
branch,
|
|
54
|
+
io: asyncIoFactory({ worktree }),
|
|
55
|
+
});
|
|
56
|
+
} catch { /* swallow */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function createAndFinaliseBranch(kind, built, worktree) {
|
|
60
|
+
const createErr = await tryCreateBranch(built.name, worktree);
|
|
61
|
+
if (createErr) return createErr;
|
|
62
|
+
if (kind === KIND_DRY_RUN)
|
|
63
|
+
await tryTruncateTrace(built.name, worktree);
|
|
64
|
+
return JSON.stringify({ ok: true, branch: built.name });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function executeGitBranch(args, context) {
|
|
68
|
+
const io = makeIO(context.worktree);
|
|
69
|
+
const stageGuard = requireNoActiveStage(io);
|
|
70
|
+
if (!stageGuard.ok)
|
|
71
|
+
return refuse(`foundry_git_branch ${stageGuard.error}`);
|
|
72
|
+
|
|
73
|
+
const validationErr = validateBranchCreationArgs(args);
|
|
74
|
+
if (validationErr) return refuse(validationErr);
|
|
75
|
+
|
|
76
|
+
const branch = currentBranch({ exec: makeExec(context.worktree) });
|
|
77
|
+
const startErr = validateStartingBranch(args.kind, branch);
|
|
78
|
+
if (startErr) return refuse(`foundry_git_branch: ${startErr}`);
|
|
79
|
+
|
|
80
|
+
const built = buildBranchName(args.kind, args, branch);
|
|
81
|
+
if (built.error) return refuse(`foundry_git_branch: ${built.error}`);
|
|
82
|
+
|
|
83
|
+
return createAndFinaliseBranch(args.kind, built, context.worktree);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// -- foundry_git_finish helpers --
|
|
87
|
+
|
|
88
|
+
function refuseBaseBranchForDryRun() {
|
|
89
|
+
return refuse(
|
|
90
|
+
'foundry_git_finish: baseBranch is not valid for a dry-run ' +
|
|
91
|
+
'finish; the parent config branch is determined by the dry-run ' +
|
|
92
|
+
'branch name.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function makeNoopResult(base) {
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
ok: true,
|
|
98
|
+
noop: true,
|
|
99
|
+
message: `Already on ${base} — nothing to merge`,
|
|
100
|
+
branch: base,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function refuseUnknownFinishBranch(branch) {
|
|
105
|
+
return refuse(
|
|
106
|
+
`foundry_git_finish: nothing to finish on '${branch || 'detached HEAD'}' ` +
|
|
107
|
+
`(expected work/<x>, config/<x>, or dry-run/<x>/<y>).`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function routeDryRunFinish(branch, args, cwd) {
|
|
111
|
+
if (args.baseBranch !== undefined)
|
|
112
|
+
return refuseBaseBranchForDryRun();
|
|
113
|
+
return finishDryRunBranch({ branch, args, cwd });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function routeBranchFinish(kind, branch, base, cwd, args) {
|
|
117
|
+
if (kind === 'work')
|
|
118
|
+
return finishWorkBranch({ workBranch: branch, base, cwd, args });
|
|
119
|
+
if (kind === 'config')
|
|
120
|
+
return finishConfigBranch({ configBranch: branch, base, cwd, args });
|
|
121
|
+
return refuseUnknownFinishBranch(branch);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function executeGitFinish(args, context) {
|
|
125
|
+
const io = makeIO(context.worktree);
|
|
126
|
+
const stageGuard = requireNoActiveStage(io);
|
|
127
|
+
if (!stageGuard.ok)
|
|
128
|
+
return refuse(`foundry_git_finish ${stageGuard.error}`);
|
|
129
|
+
|
|
130
|
+
const cwd = context.worktree;
|
|
131
|
+
const branch = currentBranch({ exec: makeExec(cwd) });
|
|
132
|
+
const kind = classifyBranch(branch);
|
|
133
|
+
|
|
134
|
+
if (kind === 'dry-run')
|
|
135
|
+
return routeDryRunFinish(branch, args, cwd);
|
|
136
|
+
|
|
137
|
+
const base = args.baseBranch || 'main';
|
|
138
|
+
if (branch === base) return makeNoopResult(base);
|
|
139
|
+
return routeBranchFinish(kind, branch, base, cwd, args);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// -- Tool definitions --
|
|
143
|
+
|
|
144
|
+
export function createGitTools({ tool }) {
|
|
145
|
+
return {
|
|
146
|
+
foundry_git_branch: tool({
|
|
147
|
+
description:
|
|
148
|
+
'Create and checkout a foundry branch. Requires `kind`: ' +
|
|
149
|
+
'"config" (schema work, off main), "work" (flow run, off main), ' +
|
|
150
|
+
'or "dry-run" (flow run off a config/* branch).',
|
|
151
|
+
args: {
|
|
152
|
+
kind: tool.schema.string().describe('config | work | dry-run'),
|
|
153
|
+
flowId: tool.schema.string().optional()
|
|
154
|
+
.describe('Flow ID. Required for kind="work" and "dry-run"; not valid for kind="config".'),
|
|
155
|
+
description: tool.schema.string()
|
|
156
|
+
.describe('Slugified description suffix.'),
|
|
157
|
+
},
|
|
158
|
+
async execute(args, context) {
|
|
159
|
+
return executeGitBranch(args, context);
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
foundry_git_finish: tool({
|
|
164
|
+
description:
|
|
165
|
+
'Finish the current foundry branch. ' +
|
|
166
|
+
'work/<x>: squash-merge + WORK cleanup. ' +
|
|
167
|
+
'config/<x>: squash-merge. ' +
|
|
168
|
+
'dry-run/<x>/<y>: snapshot + discard.',
|
|
169
|
+
args: {
|
|
170
|
+
message: tool.schema.string().describe('Squash merge / snapshot message'),
|
|
171
|
+
baseBranch: tool.schema.string().optional()
|
|
172
|
+
.describe('Target branch (default: main). Not valid for dry-run finish.'),
|
|
173
|
+
confirm: tool.schema.boolean().optional()
|
|
174
|
+
.describe('Must be true to perform destructive operations; otherwise returns a plan'),
|
|
175
|
+
},
|
|
176
|
+
async execute(args, context) {
|
|
177
|
+
return executeGitFinish(args, context);
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
}
|