devflow-kit 3.9.0 → 3.10.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/CHANGELOG.md +111 -0
- package/README.md +1 -1
- package/dist/agents/code.md +6 -6
- package/dist/agents/fix.md +6 -6
- package/dist/agents/harden.md +7 -6
- package/dist/agents/refactor.md +6 -6
- package/dist/cli/commands/ambient.js +10 -2
- package/dist/cli/commands/compliance-prompts.js +9 -4
- package/dist/cli/commands/compliance.js +171 -24
- package/dist/cli/commands/curation.js +12 -7
- package/dist/cli/commands/gate-level-prompts.js +39 -25
- package/dist/cli/commands/gates.js +5 -7
- package/dist/cli/commands/init.js +142 -138
- package/dist/cli/commands/install-report.js +34 -0
- package/dist/cli/commands/legacy-sweep.js +64 -0
- package/dist/cli/commands/proxy.js +6 -1
- package/dist/cli/commands/rules.js +29 -12
- package/dist/cli/commands/skills.js +42 -9
- package/dist/cli/commands/tracker.js +9 -3
- package/dist/cli/commands/uninstall.js +277 -51
- package/dist/commands/bug-analysis.md +69 -120
- package/dist/commands/dynamic-build.md +23 -23
- package/dist/commands/implement.md +46 -32
- package/dist/commands/release.md +1 -1
- package/dist/commands/resolve.md +35 -31
- package/dist/commands/self-review.md +8 -7
- package/dist/compliance-off/agents/git.md +120 -0
- package/dist/compliance-off/agents/triage.md +180 -0
- package/dist/compliance-off/commands/bug-analysis.md +398 -0
- package/dist/compliance-off/commands/code-review.md +520 -0
- package/dist/compliance-off/commands/dynamic-build.md +1251 -0
- package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
- package/dist/compliance-off/commands/implement.md +736 -0
- package/dist/compliance-off/commands/plan.md +671 -0
- package/dist/compliance-off/commands/release.md +309 -0
- package/dist/compliance-off/commands/resolve.md +742 -0
- package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
- package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
- package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
- package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
- package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
- package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
- package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
- package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
- package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
- package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
- package/dist/core/agent-models.js +19 -4
- package/dist/core/assets.js +126 -17
- package/dist/core/compliance-off-files.js +27 -0
- package/dist/core/curation-variants.js +460 -112
- package/dist/core/flags.js +2 -2
- package/dist/core/installed-variants.js +63 -0
- package/dist/core/manifest.js +48 -5
- package/dist/core/mds-variants.js +700 -3
- package/dist/core/orphan-sweep.js +11 -2
- package/dist/core/plugins.js +65 -8
- package/dist/curation-compliance-off/agents/triage.md +176 -0
- package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
- package/dist/curation-compliance-off/commands/code-review.md +481 -0
- package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
- package/dist/curation-compliance-off/commands/implement.md +689 -0
- package/dist/curation-compliance-off/commands/plan.md +631 -0
- package/dist/curation-compliance-off/commands/release.md +279 -0
- package/dist/curation-compliance-off/commands/resolve.md +692 -0
- package/dist/curation-off/agents/code.md +6 -6
- package/dist/curation-off/agents/fix.md +6 -6
- package/dist/curation-off/agents/harden.md +7 -6
- package/dist/curation-off/agents/refactor.md +6 -6
- package/dist/curation-off/commands/bug-analysis.md +69 -119
- package/dist/curation-off/commands/dynamic-build.md +23 -23
- package/dist/curation-off/commands/implement.md +46 -32
- package/dist/curation-off/commands/release.md +1 -1
- package/dist/curation-off/commands/resolve.md +35 -31
- package/dist/curation-off/commands/self-review.md +8 -7
- package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
- package/dist/targets/claude-code/behavior-install.js +23 -9
- package/dist/targets/claude-code/charter-render.js +374 -0
- package/dist/targets/claude-code/compliance-install.js +32 -12
- package/dist/targets/claude-code/compliance-off-files.js +245 -0
- package/dist/targets/claude-code/curation-install.js +570 -100
- package/dist/targets/claude-code/installer.js +467 -62
- package/dist/targets/claude-code/language-stamp.js +6 -0
- package/dist/targets/claude-code/linked-folder.js +182 -0
- package/dist/targets/claude-code/reference-converge.js +118 -0
- package/dist/targets/claude-code/tracker-install.js +21 -8
- package/dist/targets/claude-code/tracker-servers.js +20 -6
- package/dist/targets/claude-code/workflow-folder-install.js +231 -0
- package/dist/variant-axes.json +109 -0
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +6 -6
- package/src/assets/agents/evaluate.md +1 -1
- package/src/assets/agents/git.mds +32 -0
- package/src/assets/agents/harden.mds +7 -6
- package/src/assets/agents/test.md +21 -12
- package/src/assets/agents/triage.mds +2 -0
- package/src/assets/agents/validate.md +10 -10
- package/src/assets/commands/_partials/_compliance.mds +5 -3
- package/src/assets/commands/_partials/_engine.mds +9 -8
- package/src/assets/commands/_partials/_publication.mds +10 -1
- package/src/assets/commands/bug-analysis.mds +80 -126
- package/src/assets/commands/code-review.mds +43 -0
- package/src/assets/commands/dynamic-build.mds +109 -15
- package/src/assets/commands/dynamic-tickets.mds +13 -0
- package/src/assets/commands/implement.mds +145 -33
- package/src/assets/commands/plan.mds +19 -0
- package/src/assets/commands/release.mds +50 -1
- package/src/assets/commands/resolve.mds +217 -32
- package/src/assets/commands/self-review.mds +8 -7
- package/src/assets/mds/git/_ops.mds +23 -0
- package/src/assets/mds/git/_ops_steps.mds +17 -0
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/mds/git/_references.mds +6 -0
- package/src/assets/mds/tracker/_common.mds +4 -0
- package/src/assets/mds/tracker/_github.mds +42 -0
- package/src/assets/mds/tracker/_jira.mds +43 -0
- package/src/assets/mds/tracker/_linear.mds +43 -0
- package/src/assets/rules/quality.md +2 -0
- package/src/assets/scripts/ci-wait.cjs +204 -126
- package/src/assets/scripts/hooks/assets/implement-installed +1 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
- package/src/assets/scripts/hooks/preamble +18 -8
- package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
- package/src/assets/scripts/pr-evidence.cjs +41 -2
- package/src/assets/scripts/verify-evidence.cjs +408 -56
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +7 -7
- package/src/assets/skills/docs-framework/SKILL.md +5 -2
- package/src/assets/skills/git/SKILL.md +3 -3
- package/src/assets/skills/git/references/github-api.md +2 -139
- package/src/assets/skills/git/references/github-posting.md +158 -0
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +1 -1
- package/src/assets/skills/review-methodology/references/report-template.md +1 -2
- package/src/assets/skills/testing/SKILL.md +2 -4
|
@@ -44,6 +44,7 @@ import * as path from 'path';
|
|
|
44
44
|
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
45
45
|
import { mdFileName } from '../../core/orphan-sweep.js';
|
|
46
46
|
import { installedLanguageFocuses } from '../../core/plugins.js';
|
|
47
|
+
import { isUnrealFolder } from './linked-folder.js';
|
|
47
48
|
/** The fixed prefix of the stamp line; the prompt and the anchored match share it. */
|
|
48
49
|
export const LANGUAGE_STAMP_PREFIX = 'Installed language focuses: ';
|
|
49
50
|
/** What an empty list renders as, and what the build ships. */
|
|
@@ -138,6 +139,11 @@ export async function restampInstalledCommands(opts) {
|
|
|
138
139
|
warn(`language stamp: claudeDir is not an absolute path ("${claudeDir}") — the installed commands were not stamped`);
|
|
139
140
|
return { focuses, rewritten, unchanged, absent, failed: [...LANGUAGE_STAMPED_COMMANDS] };
|
|
140
141
|
}
|
|
142
|
+
// Whatever the caller's guard says, nothing is read or written through a commands folder that is a
|
|
143
|
+
// link or not a folder; every stamped command is then left as it is, and `failed` says so.
|
|
144
|
+
if (await isUnrealFolder(path.join(claudeDir, 'commands', 'devflow'))) {
|
|
145
|
+
return { focuses, rewritten, unchanged, absent, failed: [...LANGUAGE_STAMPED_COMMANDS] };
|
|
146
|
+
}
|
|
141
147
|
for (const command of LANGUAGE_STAMPED_COMMANDS) {
|
|
142
148
|
const target = path.join(claudeDir, 'commands', 'devflow', mdFileName(command));
|
|
143
149
|
let current;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
3
|
+
/** Classify `dir` with `lstat`, so a link is seen as a link and never as the directory it points to. */
|
|
4
|
+
export async function directoryState(dir) {
|
|
5
|
+
try {
|
|
6
|
+
const stat = await fs.lstat(dir);
|
|
7
|
+
if (stat.isSymbolicLink())
|
|
8
|
+
return 'link';
|
|
9
|
+
return stat.isDirectory() ? 'real' : 'unusable';
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return err.code === 'ENOENT' ? 'absent' : 'unusable';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Whether `dir` is a link or not a folder, so that a run must not write into it. An absent folder is fine: the run creates it. */
|
|
16
|
+
export async function isUnrealFolder(dir) {
|
|
17
|
+
const state = await directoryState(dir);
|
|
18
|
+
return state === 'link' || state === 'unusable';
|
|
19
|
+
}
|
|
20
|
+
/** Whether a link stands at `entry`. An absent or unreadable entry is not a link. */
|
|
21
|
+
export async function entryIsLink(entry) {
|
|
22
|
+
return (await directoryState(entry)) === 'link';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Remove the link at `entry` with `unlink`: the link, never what it points to.
|
|
26
|
+
*
|
|
27
|
+
* The one place the pre-clean replaces a link. `readdir` and `mkdir` follow a link, so a pre-clean
|
|
28
|
+
* that emptied through one would empty another directory. Never throws; a link that cannot be
|
|
29
|
+
* removed is `stuck`, and the caller leaves the path alone.
|
|
30
|
+
*/
|
|
31
|
+
export async function unlinkLinkAt(entry) {
|
|
32
|
+
if (!(await entryIsLink(entry)))
|
|
33
|
+
return 'no-link';
|
|
34
|
+
try {
|
|
35
|
+
await fs.unlink(entry);
|
|
36
|
+
return 'unlinked';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 'stuck';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Remove `target` whole. A link there is unlinked instead and returned, so the caller can report it;
|
|
44
|
+
* the folder it points to is left as it is. A failure of the final `rm` is thrown.
|
|
45
|
+
*/
|
|
46
|
+
export async function removeReportingLink(target) {
|
|
47
|
+
const outcome = await unlinkLinkAt(target);
|
|
48
|
+
if (outcome === 'no-link')
|
|
49
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
50
|
+
return outcome === 'unlinked' ? [target] : [];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Prove, with {@link isUnrealFolder}, that a run may write into `dir`, and record the folder in
|
|
54
|
+
* `report.refusedFolders` when it may not. Returns true for a refused folder, so the caller skips
|
|
55
|
+
* every write into it.
|
|
56
|
+
*
|
|
57
|
+
* The answer for each folder a run writes into besides the `devflow:git` references, which the
|
|
58
|
+
* installer's `referencesHostState` answers: the commands, rules and agents folders and every other
|
|
59
|
+
* skill folder. An absent folder is fine, since the install creates it. A link is refused and never
|
|
60
|
+
* repaired, because the installer did not create it. A full install's pre-clean has already replaced
|
|
61
|
+
* the links it could, so this refuses only what is left. Never throws.
|
|
62
|
+
*/
|
|
63
|
+
export async function refuseUnrealFolder(report, dir) {
|
|
64
|
+
if (!(await isUnrealFolder(dir)))
|
|
65
|
+
return false;
|
|
66
|
+
report.refusedFolders.push(dir);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Make `entry` safe to write: a link there is unlinked, never followed, so the caller then creates a
|
|
71
|
+
* real file or folder in its place. The entry can be a file or a folder. Only the last component is
|
|
72
|
+
* checked, so a caller checks each folder on the way down before it checks an entry in it.
|
|
73
|
+
*
|
|
74
|
+
* Fail closed: an entry `lstat` cannot inspect is refused. Never throws.
|
|
75
|
+
*/
|
|
76
|
+
export async function clearLinkedEntry(entry) {
|
|
77
|
+
try {
|
|
78
|
+
const stat = await fs.lstat(entry);
|
|
79
|
+
if (!stat.isSymbolicLink())
|
|
80
|
+
return 'clear';
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return err.code === 'ENOENT' ? 'clear' : 'refused';
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await fs.unlink(entry);
|
|
87
|
+
return 'replaced';
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return 'refused';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* {@link clearLinkedEntry} plus the report: a replaced link goes to `report.replacedLinks`, a refused
|
|
95
|
+
* entry to `report.refusedFolders`. Returns true when the caller may write at `entry`.
|
|
96
|
+
*
|
|
97
|
+
* The one call a write step makes before it creates or overwrites a file or folder inside a Devflow
|
|
98
|
+
* folder. Omit `report` when the step has none; the link is still never written through.
|
|
99
|
+
*/
|
|
100
|
+
export async function clearLinkForWrite(entry, report) {
|
|
101
|
+
const clearance = await clearLinkedEntry(entry);
|
|
102
|
+
if (clearance === 'replaced')
|
|
103
|
+
report?.replacedLinks.push(entry);
|
|
104
|
+
if (clearance === 'refused')
|
|
105
|
+
report?.refusedFolders.push(entry);
|
|
106
|
+
return clearance !== 'refused';
|
|
107
|
+
}
|
|
108
|
+
/** A report that holds no link yet. */
|
|
109
|
+
export function newLinkReport() {
|
|
110
|
+
return { replacedLinks: [], refusedFolders: [] };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One report from several: the paths of every report in order, each path once. A command that gets
|
|
114
|
+
* the links of more than one step prints this, so a folder that two steps met is named once.
|
|
115
|
+
* Returns a new report; the inputs are not changed.
|
|
116
|
+
*/
|
|
117
|
+
export function mergeLinkReports(...reports) {
|
|
118
|
+
return {
|
|
119
|
+
replacedLinks: [...new Set(reports.flatMap(report => report.replacedLinks))],
|
|
120
|
+
refusedFolders: [...new Set(reports.flatMap(report => report.refusedFolders))],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The line for a link a step replaced. One wording for the install summary and for every converge
|
|
125
|
+
* that reports through a `warn` callback.
|
|
126
|
+
*/
|
|
127
|
+
export function replacedLinkMessage(linkPath) {
|
|
128
|
+
return `${linkPath} was a link. It was replaced with a real file or folder, and what it pointed to was left as it was.`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The first sentence of every refused-folder line: what the folder is. The install and the uninstall
|
|
132
|
+
* both open with it and then say what they did and what to do.
|
|
133
|
+
*/
|
|
134
|
+
export function refusedFolderStatement(folder) {
|
|
135
|
+
return `${folder} is a link or not a folder.`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The line for a folder a step wrote nothing at, because it is a link it could not remove or not a
|
|
139
|
+
* folder. One wording for the install summary and for every converge that reports through a `warn`
|
|
140
|
+
* callback.
|
|
141
|
+
*/
|
|
142
|
+
export function refusedFolderMessage(folder) {
|
|
143
|
+
return `${refusedFolderStatement(folder)} Nothing was written there. Replace it with a real file or folder and run the install again.`;
|
|
144
|
+
}
|
|
145
|
+
/** Send one line per link in `report` to `warn`: the replaced links first, then the refused folders. */
|
|
146
|
+
export function warnLinkReport(report, warn) {
|
|
147
|
+
for (const linkPath of report.replacedLinks)
|
|
148
|
+
warn(replacedLinkMessage(linkPath));
|
|
149
|
+
for (const folder of report.refusedFolders)
|
|
150
|
+
warn(refusedFolderMessage(folder));
|
|
151
|
+
}
|
|
152
|
+
/** The mode of a file that replaces a link. The atomic writer would copy the mode of the link's target instead. */
|
|
153
|
+
const REPLACED_LINK_FILE_MODE = 0o644;
|
|
154
|
+
/**
|
|
155
|
+
* Write `content` at `file` through a temp file beside it and one rename. A rename puts the new file
|
|
156
|
+
* in the place of whatever stands at `file`, so a link there is replaced and never followed; the link
|
|
157
|
+
* is recorded in `links.replacedLinks` once the write is done. A file that replaces a link has the
|
|
158
|
+
* fixed mode 0644, never the mode of what the link pointed to, which the atomic writer would copy
|
|
159
|
+
* because its `stat` follows the link. Any other file goes through the atomic writer and keeps its
|
|
160
|
+
* own mode. The caller has proved that the folder holding `file` is a real one. Throws what the
|
|
161
|
+
* writer throws, so the caller keeps its own catch.
|
|
162
|
+
*/
|
|
163
|
+
export async function writeFileReplacingLink(file, content, links) {
|
|
164
|
+
if (!(await entryIsLink(file))) {
|
|
165
|
+
await writeFileAtomicExclusive(file, content);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const temp = `${file}.tmp.${process.pid}`;
|
|
169
|
+
try {
|
|
170
|
+
// A stale temp file is removed first; the exclusive open then refuses anything placed there since.
|
|
171
|
+
await fs.rm(temp, { force: true });
|
|
172
|
+
await fs.writeFile(temp, content, { encoding: 'utf-8', flag: 'wx', mode: REPLACED_LINK_FILE_MODE });
|
|
173
|
+
await fs.chmod(temp, REPLACED_LINK_FILE_MODE);
|
|
174
|
+
await fs.rename(temp, file);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
await fs.rm(temp, { force: true }).catch(() => undefined);
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
links?.replacedLinks.push(file);
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=linked-folder.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The references kind of the variant converge: make the installed `devflow:git` references
|
|
3
|
+
* match the machine's compliance switch (AC-41, AC-50).
|
|
4
|
+
*
|
|
5
|
+
* `devflow init`, `devflow compliance` and `devflow curation` reach it through
|
|
6
|
+
* convergeVariantAxes, so a compliance toggle needs no reinstall. It does, in order:
|
|
7
|
+
*
|
|
8
|
+
* 1. the overlay, with the manifest and the ONE source root of the switch
|
|
9
|
+
* (`installedReferenceManifest({ compliance })`, `skillRefsSourceDir`): it rewrites the
|
|
10
|
+
* references whose text differs between the two states and prunes the omitted
|
|
11
|
+
* operation files from `pr/` and `tracker/`. With compliance off the source root is the
|
|
12
|
+
* compliance-off tree, and an absent root is a failure that writes nothing: the converge
|
|
13
|
+
* never falls through to the compliance-on references;
|
|
14
|
+
* 2. with compliance off, the removal list ({@link removeComplianceOffFiles});
|
|
15
|
+
* with compliance on, the restore of the files the skill copy puts in place
|
|
16
|
+
* ({@link restoreComplianceOffFiles}), from the source the install would copy.
|
|
17
|
+
*
|
|
18
|
+
* It acts on a skill that is ALREADY installed. A machine whose plugin selection holds no
|
|
19
|
+
* `devflow:git` has nothing to converge, and that is a clean result. A skill directory, or a
|
|
20
|
+
* `references` directory inside it, that is a link or not a directory is a failure: the overlay
|
|
21
|
+
* would stage, prune and set modes through it. A full install empties the skill directory first and
|
|
22
|
+
* so replaces such a link; this converge and a partial install have no such step, so each refuses
|
|
23
|
+
* instead, on the same proof ({@link referencesHostState}).
|
|
24
|
+
*
|
|
25
|
+
* Never throws. Every failure is a warning and a `failures` entry, and the next step still
|
|
26
|
+
* runs. With compliance off a caller reads `failures` and `stillPosting` and fails closed.
|
|
27
|
+
*/
|
|
28
|
+
import * as path from 'path';
|
|
29
|
+
import { skillRefsSourceDir } from '../../core/assets.js';
|
|
30
|
+
import { installedReferenceManifest, SKILL_REFS_SKILL_NAME } from '../../core/mds-variants.js';
|
|
31
|
+
import { getPackageRoot } from '../../core/paths.js';
|
|
32
|
+
import { prefixSkillName } from '../../core/plugins.js';
|
|
33
|
+
import { removeComplianceOffFiles, restoreComplianceOffFiles } from './compliance-off-files.js';
|
|
34
|
+
import { overlayGeneratedReferences, overlayUnitLabel, referencesHostState, resolveSkillSource } from './installer.js';
|
|
35
|
+
export async function convergeReferences(opts) {
|
|
36
|
+
const { claudeDir, devflowDir, compliance, warn } = opts;
|
|
37
|
+
const failures = [];
|
|
38
|
+
const fail = (message) => {
|
|
39
|
+
warn(`reference converge: ${message}`);
|
|
40
|
+
failures.push(message);
|
|
41
|
+
};
|
|
42
|
+
// Precondition, asserted in production code: a relative claudeDir would resolve every
|
|
43
|
+
// install target somewhere unexpected, the removals included.
|
|
44
|
+
if (!path.isAbsolute(claudeDir)) {
|
|
45
|
+
fail(`claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
|
|
46
|
+
return { converged: false, rewritten: [], removed: [], restored: [], failures, stillPosting: [], unproven: !compliance };
|
|
47
|
+
}
|
|
48
|
+
const root = opts.packageRoot ?? getPackageRoot();
|
|
49
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
50
|
+
const gitSkillDir = path.join(skillsDir, prefixSkillName(SKILL_REFS_SKILL_NAME));
|
|
51
|
+
const manifest = installedReferenceManifest({ compliance });
|
|
52
|
+
let rewritten = [];
|
|
53
|
+
let unproven = false;
|
|
54
|
+
const host = await referencesHostState(gitSkillDir);
|
|
55
|
+
if (host.kind === 'refused') {
|
|
56
|
+
fail(`${host.path} is not a real directory, so the references in it were not converged`);
|
|
57
|
+
unproven = true;
|
|
58
|
+
}
|
|
59
|
+
else if (host.kind === 'ready') {
|
|
60
|
+
try {
|
|
61
|
+
const overlay = await overlayGeneratedReferences({
|
|
62
|
+
referencesTarget: path.join(gitSkillDir, 'references'),
|
|
63
|
+
sourceRoot: skillRefsSourceDir(compliance, root),
|
|
64
|
+
manifest,
|
|
65
|
+
warn,
|
|
66
|
+
});
|
|
67
|
+
rewritten = overlay.overlaidRefs;
|
|
68
|
+
for (const failure of overlay.overlayFailures) {
|
|
69
|
+
fail(`${overlayUnitLabel(failure.unit)} was not refreshed (${failure.error})`);
|
|
70
|
+
unproven = true;
|
|
71
|
+
}
|
|
72
|
+
for (const failed of overlay.pruned.failed) {
|
|
73
|
+
fail(`could not prune ${failed.name} (${String(failed.error)})`);
|
|
74
|
+
unproven = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// The overlay throws for a packaging failure only: an absent source root, or a manifest entry
|
|
79
|
+
// the root lacks. Nothing is written in either case, and nothing falls through to another root.
|
|
80
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
81
|
+
unproven = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const removed = [];
|
|
85
|
+
const restored = [];
|
|
86
|
+
const stillPosting = [];
|
|
87
|
+
if (!compliance) {
|
|
88
|
+
const removal = await removeComplianceOffFiles(skillsDir);
|
|
89
|
+
removed.push(...removal.removed);
|
|
90
|
+
for (const refusal of removal.refused) {
|
|
91
|
+
fail(`${refusal.entry} was not removed — ${refusal.reason}`);
|
|
92
|
+
stillPosting.push(path.join(skillsDir, ...refusal.entry.split('/')));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// The overlay owns every generated reference (publication-gate.md among them), so the restore
|
|
97
|
+
// writes back only the files that come from the skill source.
|
|
98
|
+
const ownedByOverlay = new Set(manifest.map(rel => `${prefixSkillName(SKILL_REFS_SKILL_NAME)}/references/${rel}`));
|
|
99
|
+
const restoration = await restoreComplianceOffFiles({
|
|
100
|
+
skillsDir,
|
|
101
|
+
sourceDirOf: async (skill) => (await resolveSkillSource(skill, devflowDir, opts.packageRoot)).dir,
|
|
102
|
+
ownedElsewhere: ownedByOverlay,
|
|
103
|
+
});
|
|
104
|
+
restored.push(...restoration.restored);
|
|
105
|
+
for (const refusal of restoration.refused)
|
|
106
|
+
fail(`${refusal.entry} was not restored — ${refusal.reason}`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
converged: failures.length === 0,
|
|
110
|
+
rewritten,
|
|
111
|
+
removed,
|
|
112
|
+
restored,
|
|
113
|
+
failures,
|
|
114
|
+
stillPosting: compliance ? [] : stillPosting,
|
|
115
|
+
unproven: compliance ? false : unproven,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=reference-converge.js.map
|
|
@@ -15,6 +15,7 @@ import { stampAgentTools } from '../../core/agent-frontmatter.js';
|
|
|
15
15
|
import { agentSourceDirs } from '../../core/assets.js';
|
|
16
16
|
import { mdFileName } from '../../core/orphan-sweep.js';
|
|
17
17
|
import { TRACKER_MCP_SERVERS_DEFAULT, trackerServerTools } from '../../core/tracker.js';
|
|
18
|
+
import { newLinkReport, refuseUnrealFolder, warnLinkReport, writeFileReplacingLink } from './linked-folder.js';
|
|
18
19
|
/**
|
|
19
20
|
* The agent this module owns.
|
|
20
21
|
*
|
|
@@ -97,55 +98,67 @@ async function writeWouldChangeNothing(wanted, target) {
|
|
|
97
98
|
* nothing reports `unchanged` and the summary stays quiet
|
|
98
99
|
* (see {@link writeWouldChangeNothing}).
|
|
99
100
|
*
|
|
101
|
+
* An `agents/devflow` that is a link, or not a folder, is refused: nothing is written there, and the
|
|
102
|
+
* folder is named in `refusedFolders` and in a warning. A `tracker.md` that is a link is replaced by a
|
|
103
|
+
* real file, and named in `replacedLinks` and in a warning.
|
|
104
|
+
*
|
|
100
105
|
* Never throws. A caller reads `converged`; it does not catch. The one refusal
|
|
101
106
|
* that is not an I/O degradation — a claudeDir that is not absolute — is reported
|
|
102
107
|
* the same way rather than thrown, because an install must not die on it.
|
|
103
108
|
*/
|
|
104
109
|
export async function convergeTrackerArtifacts(opts) {
|
|
105
110
|
const { claudeDir, warn } = opts;
|
|
111
|
+
const links = newLinkReport();
|
|
112
|
+
const finish = (converged, agent) => {
|
|
113
|
+
warnLinkReport(links, warn);
|
|
114
|
+
return { converged, agent, replacedLinks: links.replacedLinks, refusedFolders: links.refusedFolders };
|
|
115
|
+
};
|
|
106
116
|
// Precondition, asserted in production code rather than only in tests: an
|
|
107
117
|
// empty or relative claudeDir would make the target resolve somewhere
|
|
108
118
|
// unexpected.
|
|
109
119
|
if (!path.isAbsolute(claudeDir)) {
|
|
110
120
|
warn(`tracker: claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
|
|
111
|
-
return
|
|
121
|
+
return finish(false, 'unchanged');
|
|
112
122
|
}
|
|
113
123
|
const target = agentTarget(claudeDir);
|
|
124
|
+
// A link at the folder sends the write into another place, so the folder is proven first.
|
|
125
|
+
if (await refuseUnrealFolder(links, path.dirname(target)))
|
|
126
|
+
return finish(false, 'unchanged');
|
|
114
127
|
const dirs = opts.agentSourceDirs ?? agentSourceDirs();
|
|
115
128
|
const candidates = dirs.map(dir => path.join(dir, mdFileName(TRACKER_AGENT_NAME)));
|
|
116
129
|
const source = await firstExisting(candidates);
|
|
117
130
|
if (source === undefined) {
|
|
118
131
|
warn(`tracker: agent source not found for "${TRACKER_AGENT_NAME}" (searched: ${candidates.join(', ')}) — ` +
|
|
119
132
|
`run \`npm run build:mds\` if it is compiled from an .mds generator host`);
|
|
120
|
-
return
|
|
133
|
+
return finish(false, 'unchanged');
|
|
121
134
|
}
|
|
122
135
|
let wanted;
|
|
123
136
|
try {
|
|
124
137
|
const stamped = stampAgentTools(await fs.readFile(source, 'utf-8'), trackerServerTools(opts.servers ?? TRACKER_MCP_SERVERS_DEFAULT));
|
|
125
138
|
if (!stamped.ok) {
|
|
126
139
|
warn(`tracker: the Tracker agent source could not take the tracker servers (${stamped.error}) — not installed`);
|
|
127
|
-
return
|
|
140
|
+
return finish(false, 'unchanged');
|
|
128
141
|
}
|
|
129
142
|
wanted = stamped.value.content;
|
|
130
143
|
}
|
|
131
144
|
catch (err) {
|
|
132
145
|
warn(`tracker: cannot read the Tracker agent source (${source}) — ${String(err)}`);
|
|
133
|
-
return
|
|
146
|
+
return finish(false, 'unchanged');
|
|
134
147
|
}
|
|
135
148
|
// Already converged — nothing to write, and nothing for the summary to announce.
|
|
136
149
|
// The directory is not created either: an identical file at the target means it is
|
|
137
150
|
// already there (see {@link writeWouldChangeNothing}).
|
|
138
151
|
if (await writeWouldChangeNothing(wanted, target)) {
|
|
139
|
-
return
|
|
152
|
+
return finish(true, 'unchanged');
|
|
140
153
|
}
|
|
141
154
|
try {
|
|
142
155
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
143
|
-
await
|
|
156
|
+
await writeFileReplacingLink(target, wanted, links);
|
|
144
157
|
}
|
|
145
158
|
catch (err) {
|
|
146
159
|
warn(`tracker: failed to install the Tracker agent (${target}) — ${String(err)}`);
|
|
147
|
-
return
|
|
160
|
+
return finish(false, 'unchanged');
|
|
148
161
|
}
|
|
149
|
-
return
|
|
162
|
+
return finish(true, 'installed');
|
|
150
163
|
}
|
|
151
164
|
//# sourceMappingURL=tracker-install.js.map
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* a `--plugin` run that does not install Git leaves the installed, stamped copy alone.
|
|
18
18
|
* 2. `convergeTrackerArtifacts` stamps Tracker as it writes it, and compares the STAMPED text
|
|
19
19
|
* with the installed file, so a steady-state run reports `unchanged`.
|
|
20
|
-
* 3. `
|
|
20
|
+
* 3. `convergeVariantAxes` (init, `devflow curation` and `devflow compliance`) carries the
|
|
21
21
|
* installed stamp onto the variant it writes (`carryStampedTools`), the way it carries
|
|
22
22
|
* `model:` and `effort:`, so a variant switch keeps the list and a repeat run is byte-equal.
|
|
23
23
|
* 4. `devflow tracker --servers` re-stamps the installed copies at once ({@link restampTrackerAgents}).
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
import { promises as fs } from 'fs';
|
|
35
35
|
import * as path from 'path';
|
|
36
36
|
import { stampAgentTools } from '../../core/agent-frontmatter.js';
|
|
37
|
-
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
38
37
|
import { mdFileName } from '../../core/orphan-sweep.js';
|
|
39
38
|
import { trackerServerTools } from '../../core/tracker.js';
|
|
39
|
+
import { newLinkReport, refuseUnrealFolder, warnLinkReport, writeFileReplacingLink } from './linked-folder.js';
|
|
40
40
|
import { TRACKER_AGENT_NAME } from './tracker-install.js';
|
|
41
41
|
/** The registry name of the Git agent. */
|
|
42
42
|
export const GIT_AGENT_NAME = 'git';
|
|
@@ -51,7 +51,10 @@ export function isTrackerStampedAgent(agentName) {
|
|
|
51
51
|
*
|
|
52
52
|
* Rewrites only an installed copy (an absent one stays absent), compares against the bytes on
|
|
53
53
|
* disk at the moment of writing, and writes atomically only when they differ, so a run that
|
|
54
|
-
* changes nothing writes nothing.
|
|
54
|
+
* changes nothing writes nothing. An `agents/devflow` that is a link, or not a folder, is refused:
|
|
55
|
+
* nothing is written there, both agents are `failed`, and the folder is named in `refusedFolders` and
|
|
56
|
+
* in a warning. An agent file that is a link is replaced by a real file, and named in `replacedLinks`
|
|
57
|
+
* and in a warning. Never throws.
|
|
55
58
|
*/
|
|
56
59
|
export async function restampTrackerAgents(opts) {
|
|
57
60
|
const { claudeDir, servers, warn } = opts;
|
|
@@ -59,11 +62,22 @@ export async function restampTrackerAgents(opts) {
|
|
|
59
62
|
const unchanged = [];
|
|
60
63
|
const absent = [];
|
|
61
64
|
const failed = [];
|
|
65
|
+
const links = newLinkReport();
|
|
66
|
+
const finish = () => {
|
|
67
|
+
warnLinkReport(links, warn);
|
|
68
|
+
return { rewritten, unchanged, absent, failed, replacedLinks: links.replacedLinks, refusedFolders: links.refusedFolders };
|
|
69
|
+
};
|
|
62
70
|
// Precondition, asserted in production code: a relative claudeDir would resolve the targets
|
|
63
71
|
// against the working directory.
|
|
64
72
|
if (!path.isAbsolute(claudeDir)) {
|
|
65
73
|
warn(`tracker servers: claudeDir is not an absolute path ("${claudeDir}") — the installed agents were not stamped`);
|
|
66
|
-
|
|
74
|
+
failed.push(...TRACKER_STAMPED_AGENTS);
|
|
75
|
+
return finish();
|
|
76
|
+
}
|
|
77
|
+
// A link at the folder sends every write below it into another place, so the folder is proven first.
|
|
78
|
+
if (await refuseUnrealFolder(links, path.join(claudeDir, 'agents', 'devflow'))) {
|
|
79
|
+
failed.push(...TRACKER_STAMPED_AGENTS);
|
|
80
|
+
return finish();
|
|
67
81
|
}
|
|
68
82
|
const tools = trackerServerTools(servers);
|
|
69
83
|
for (const agent of TRACKER_STAMPED_AGENTS) {
|
|
@@ -94,7 +108,7 @@ export async function restampTrackerAgents(opts) {
|
|
|
94
108
|
continue;
|
|
95
109
|
}
|
|
96
110
|
try {
|
|
97
|
-
await
|
|
111
|
+
await writeFileReplacingLink(target, stamped.value.content, links);
|
|
98
112
|
rewritten.push(agent);
|
|
99
113
|
}
|
|
100
114
|
catch (err) {
|
|
@@ -102,6 +116,6 @@ export async function restampTrackerAgents(opts) {
|
|
|
102
116
|
failed.push(agent);
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
|
-
return
|
|
119
|
+
return finish();
|
|
106
120
|
}
|
|
107
121
|
//# sourceMappingURL=tracker-servers.js.map
|