devflow-kit 3.9.0 → 3.11.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 +142 -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/backup-session.js +131 -0
- package/dist/cli/commands/compliance-prompts.js +9 -4
- package/dist/cli/commands/compliance.js +184 -24
- package/dist/cli/commands/curation.js +23 -10
- package/dist/cli/commands/gate-level-prompts.js +39 -25
- package/dist/cli/commands/gates.js +5 -7
- package/dist/cli/commands/init.js +169 -184
- package/dist/cli/commands/install-report.js +129 -0
- package/dist/cli/commands/legacy-sweep.js +159 -0
- package/dist/cli/commands/proxy.js +6 -1
- package/dist/cli/commands/rules.js +75 -21
- package/dist/cli/commands/skills.js +42 -9
- package/dist/cli/commands/tracker.js +9 -3
- package/dist/cli/commands/uninstall.js +339 -75
- 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/compliance.js +14 -0
- package/dist/core/curation-variants.js +460 -112
- package/dist/core/flags.js +2 -2
- package/dist/core/fs-atomic.js +5 -1
- 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 +29 -2
- package/dist/core/path-set.js +15 -0
- package/dist/core/plugins.js +68 -10
- package/dist/core/reference-sweep.js +42 -25
- package/dist/core/retired-shipped-names.js +278 -0
- 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 +62 -17
- package/dist/targets/claude-code/compliance-off-files.js +249 -0
- package/dist/targets/claude-code/curation-install.js +632 -117
- package/dist/targets/claude-code/folder-swap.js +142 -0
- package/dist/targets/claude-code/install-backup.js +780 -0
- package/dist/targets/claude-code/installer.js +795 -199
- package/dist/targets/claude-code/language-stamp.js +6 -0
- package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
- package/dist/targets/claude-code/linked-folder.js +176 -0
- package/dist/targets/claude-code/reference-converge.js +131 -0
- package/dist/targets/claude-code/rules-folder.js +34 -0
- package/dist/targets/claude-code/shipped-set.js +160 -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 +251 -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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-time render of the ambient orchestrator charter, and the implement sentinel.
|
|
3
|
+
*
|
|
4
|
+
* D-CHARTER-INSTALL-RENDER: the packaged charter (src/assets/scripts/hooks/assets/orchestrator-charter.md)
|
|
5
|
+
* is the FULL text. It routes to workflows and agents that a machine may not install, so `devflow init`
|
|
6
|
+
* writes a copy with the lines of every uninstalled owner deleted, and the session-start hook injects that
|
|
7
|
+
* copy whole. Decided here:
|
|
8
|
+
*
|
|
9
|
+
* - The render only deletes. It inserts no text and reads no name into the output: the inventory (which
|
|
10
|
+
* commands and agents the selection installs) is used for presence checks alone. The full source is
|
|
11
|
+
* also the fallback, so a render that cannot run installs the full charter and warns; it never aborts
|
|
12
|
+
* the install.
|
|
13
|
+
* - The span table is the whole contract between the charter text and this module. Every span is counted
|
|
14
|
+
* in the ORIGINAL source before any removal, and a span that occurs 0 or more than 1 times fails the
|
|
15
|
+
* render, so wording drift in the charter surfaces at install time and in the test, never as a
|
|
16
|
+
* silently wrong charter. The edits are ranges of the original source applied once, and two chosen
|
|
17
|
+
* ranges that share bytes fail the render rather than cut through each other.
|
|
18
|
+
* - The two debug-command spans overlap on `, else ` by design (`devflow:debug, else Explore ...`). They
|
|
19
|
+
* share one owner with opposite polarity, so a render removes exactly one of them: the fallback
|
|
20
|
+
* sentence when debug is installed, the command mention when it is not.
|
|
21
|
+
* - devflow-ambient's agents count as installed whatever the selection says. The charter belongs to that
|
|
22
|
+
* plugin, and the hooks inject it only while ambient mode is on, which is when `devflow init` installs
|
|
23
|
+
* the plugin; `devflow ambient --enable` warns when it is not installed.
|
|
24
|
+
* - The sentinel (`hooks/assets/implement-installed`) exists exactly when devflow-implement is installed.
|
|
25
|
+
* The per-prompt preamble branches on its existence with a shell builtin, so the plan-handoff directive
|
|
26
|
+
* runs only where the workflow it names exists. This module is the only writer of the charter and the
|
|
27
|
+
* sentinel at install time; the verbatim script copy skips both.
|
|
28
|
+
* - `mode: 'uninstall'` is the uninstall mode. An uninstall re-renders a charter that is installed from the
|
|
29
|
+
* manifest record and may take the sentinel away, but never creates a file. With no record to render
|
|
30
|
+
* from, `removeHandoffSentinel` takes the sentinel away alone and the charter stays as it is.
|
|
31
|
+
* - A change guard is asked about a file before the file is read, and every write or removal follows it.
|
|
32
|
+
* - After a package upgrade the installed charter stays as rendered until the next `devflow init`.
|
|
33
|
+
*
|
|
34
|
+
* Never throws: every failure is a warning and a `failed` outcome, because the install must not die on a
|
|
35
|
+
* charter. Silent on success.
|
|
36
|
+
*/
|
|
37
|
+
import { promises as fs } from 'fs';
|
|
38
|
+
import * as path from 'path';
|
|
39
|
+
import { scriptsDir as packagedScriptsDir } from '../../core/assets.js';
|
|
40
|
+
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
41
|
+
import { DEVFLOW_PLUGINS, registeredPluginNames } from '../../core/plugins.js';
|
|
42
|
+
/** The charter, relative to the scripts directory (packaged and installed alike). */
|
|
43
|
+
export const CHARTER_RELPATH = 'hooks/assets/orchestrator-charter.md';
|
|
44
|
+
/** The sentinel, relative to the scripts directory. It exists exactly when devflow-implement is installed. */
|
|
45
|
+
export const HANDOFF_SENTINEL_RELPATH = 'hooks/assets/implement-installed';
|
|
46
|
+
/** The plugin whose agents the charter always treats as installed. */
|
|
47
|
+
const AMBIENT_PLUGIN = 'devflow-ambient';
|
|
48
|
+
/** The permission bits of an installed charter or sentinel: the hooks directory is installed executable. */
|
|
49
|
+
const INSTALLED_MODE = 0o755;
|
|
50
|
+
/** The command whose presence the sentinel records. */
|
|
51
|
+
const HANDOFF_COMMAND = 'implement';
|
|
52
|
+
/** The workflow list's items, in the order the charter joins them with `, `. */
|
|
53
|
+
export const CHARTER_WORKFLOW_ITEMS = [
|
|
54
|
+
{ text: 'devflow:implement', owner: { kind: 'command', name: 'implement' } },
|
|
55
|
+
{ text: 'devflow:plan', owner: { kind: 'command', name: 'plan' } },
|
|
56
|
+
{ text: 'devflow:research', owner: { kind: 'command', name: 'research' } },
|
|
57
|
+
{ text: 'devflow:explore', owner: { kind: 'command', name: 'explore' } },
|
|
58
|
+
{ text: 'devflow:code-review', owner: { kind: 'command', name: 'code-review' } },
|
|
59
|
+
{ text: 'devflow:resolve', owner: { kind: 'command', name: 'resolve' } },
|
|
60
|
+
{
|
|
61
|
+
text: 'devflow:bug-analysis (hunt bugs in a branch before merge)',
|
|
62
|
+
owner: { kind: 'command', name: 'bug-analysis' },
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
/** The joined list as the charter spells it: it occurs once, on the workflow bullet. */
|
|
66
|
+
export const CHARTER_WORKFLOW_LIST = CHARTER_WORKFLOW_ITEMS.map(item => item.text).join(', ');
|
|
67
|
+
/** Every other span: removal-only, each tied to one owner. */
|
|
68
|
+
export const CHARTER_SPANS = [
|
|
69
|
+
{
|
|
70
|
+
shape: 'text',
|
|
71
|
+
text: 'Fix (`OPERATION:` `validation-fix`, `alignment-fix`, `qa-fix` or `ci-fix`), else ',
|
|
72
|
+
owner: { kind: 'agent', name: 'fix' },
|
|
73
|
+
removedWhen: 'absent',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
shape: 'text',
|
|
77
|
+
text: 'devflow:debug, else ',
|
|
78
|
+
owner: { kind: 'command', name: 'debug' },
|
|
79
|
+
removedWhen: 'absent',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
shape: 'text',
|
|
83
|
+
text: ', else Explore investigates and Code fixes',
|
|
84
|
+
owner: { kind: 'command', name: 'debug' },
|
|
85
|
+
removedWhen: 'present',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
shape: 'text',
|
|
89
|
+
text: '; never the Debug agent',
|
|
90
|
+
owner: { kind: 'agent', name: 'debug' },
|
|
91
|
+
removedWhen: 'absent',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
shape: 'line',
|
|
95
|
+
text: '- Plan handoff: ',
|
|
96
|
+
owner: { kind: 'command', name: HANDOFF_COMMAND },
|
|
97
|
+
removedWhen: 'absent',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
/**
|
|
101
|
+
* The commands and agents a plugin selection installs.
|
|
102
|
+
*
|
|
103
|
+
* Only names in DEVFLOW_PLUGINS count: a manifest is file content, so an unknown, deleted or hostile name
|
|
104
|
+
* selects nothing, and a name that reaches here from a legacy install maps to its current plugin
|
|
105
|
+
* ({@link registeredPluginNames}). Every caller reaches this check, so it holds whatever the caller filtered.
|
|
106
|
+
* devflow-ambient's agents are always included. The names are used for presence checks only and never reach
|
|
107
|
+
* the rendered text.
|
|
108
|
+
*/
|
|
109
|
+
export function charterInventory(pluginNames) {
|
|
110
|
+
const selected = new Set(registeredPluginNames(pluginNames));
|
|
111
|
+
const commands = new Set();
|
|
112
|
+
const agents = new Set();
|
|
113
|
+
for (const plugin of DEVFLOW_PLUGINS) {
|
|
114
|
+
if (plugin.name === AMBIENT_PLUGIN) {
|
|
115
|
+
for (const agent of plugin.agents)
|
|
116
|
+
agents.add(agent);
|
|
117
|
+
}
|
|
118
|
+
if (!selected.has(plugin.name))
|
|
119
|
+
continue;
|
|
120
|
+
for (const command of plugin.commands)
|
|
121
|
+
commands.add(command.startsWith('/') ? command.slice(1) : command);
|
|
122
|
+
for (const agent of plugin.agents)
|
|
123
|
+
agents.add(agent);
|
|
124
|
+
}
|
|
125
|
+
return { commands, agents };
|
|
126
|
+
}
|
|
127
|
+
/** How many times `needle` occurs in `haystack`, overlapping occurrences included. Bounded by the haystack length. */
|
|
128
|
+
function occurrencesOf(haystack, needle) {
|
|
129
|
+
if (needle.length === 0)
|
|
130
|
+
return 0;
|
|
131
|
+
let count = 0;
|
|
132
|
+
for (let from = haystack.indexOf(needle); from !== -1; from = haystack.indexOf(needle, from + 1))
|
|
133
|
+
count++;
|
|
134
|
+
return count;
|
|
135
|
+
}
|
|
136
|
+
/** The range of the whole line that holds `index`, its newline included. */
|
|
137
|
+
function lineRangeAt(source, index) {
|
|
138
|
+
const start = index === 0 ? 0 : source.lastIndexOf('\n', index - 1) + 1;
|
|
139
|
+
const newline = source.indexOf('\n', index);
|
|
140
|
+
return { start, end: newline === -1 ? source.length : newline + 1 };
|
|
141
|
+
}
|
|
142
|
+
/** The one place `span` sits in `source`, or the way it fails to. */
|
|
143
|
+
function locateSpan(source, span) {
|
|
144
|
+
if (span.shape === 'text') {
|
|
145
|
+
const count = occurrencesOf(source, span.text);
|
|
146
|
+
if (count !== 1)
|
|
147
|
+
return { reason: count === 0 ? 'missing' : 'repeated' };
|
|
148
|
+
const start = source.indexOf(span.text);
|
|
149
|
+
return { start, end: start + span.text.length };
|
|
150
|
+
}
|
|
151
|
+
const starts = [];
|
|
152
|
+
let lineStart = 0;
|
|
153
|
+
for (const line of source.split('\n')) {
|
|
154
|
+
if (line.startsWith(span.text))
|
|
155
|
+
starts.push(lineStart);
|
|
156
|
+
lineStart += line.length + 1;
|
|
157
|
+
}
|
|
158
|
+
if (starts.length !== 1)
|
|
159
|
+
return { reason: starts.length === 0 ? 'missing' : 'repeated' };
|
|
160
|
+
return lineRangeAt(source, starts[0]);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* `source` with the spans of every uninstalled owner deleted.
|
|
164
|
+
*
|
|
165
|
+
* Pure. Counts every span in the original source first and fails on a span that occurs 0 or more than 1
|
|
166
|
+
* times; a render that cannot be trusted returns `{ok: false}` and the caller keeps the full source. The
|
|
167
|
+
* workflow list keeps its installed items in order (with none kept, the whole bullet line goes), every
|
|
168
|
+
* other span goes when its owner is absent (or, for the debug fallback, present).
|
|
169
|
+
*/
|
|
170
|
+
export function renderCharter(source, installedCommands, installedAgents) {
|
|
171
|
+
const installed = (owner) => (owner.kind === 'command' ? installedCommands : installedAgents).has(owner.name);
|
|
172
|
+
// 1. Place every span in the original source, before anything is removed.
|
|
173
|
+
const listCount = occurrencesOf(source, CHARTER_WORKFLOW_LIST);
|
|
174
|
+
if (listCount !== 1) {
|
|
175
|
+
return { ok: false, reason: listCount === 0 ? 'missing' : 'repeated', span: CHARTER_WORKFLOW_LIST };
|
|
176
|
+
}
|
|
177
|
+
const listStart = source.indexOf(CHARTER_WORKFLOW_LIST);
|
|
178
|
+
const placed = [];
|
|
179
|
+
for (const span of CHARTER_SPANS) {
|
|
180
|
+
const at = locateSpan(source, span);
|
|
181
|
+
if ('reason' in at)
|
|
182
|
+
return { ok: false, reason: at.reason, span: span.text };
|
|
183
|
+
placed.push({ span, at });
|
|
184
|
+
}
|
|
185
|
+
// 2. Decide the edits, all against the original source.
|
|
186
|
+
const edits = [];
|
|
187
|
+
const kept = CHARTER_WORKFLOW_ITEMS.filter(item => installed(item.owner));
|
|
188
|
+
if (kept.length === 0) {
|
|
189
|
+
edits.push({ ...lineRangeAt(source, listStart), replacement: '', span: CHARTER_WORKFLOW_LIST });
|
|
190
|
+
}
|
|
191
|
+
else if (kept.length < CHARTER_WORKFLOW_ITEMS.length) {
|
|
192
|
+
edits.push({
|
|
193
|
+
start: listStart,
|
|
194
|
+
end: listStart + CHARTER_WORKFLOW_LIST.length,
|
|
195
|
+
replacement: kept.map(item => item.text).join(', '),
|
|
196
|
+
span: CHARTER_WORKFLOW_LIST,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
for (const { span, at } of placed) {
|
|
200
|
+
const present = installed(span.owner);
|
|
201
|
+
if (span.removedWhen === 'absent' ? !present : present) {
|
|
202
|
+
edits.push({ start: at.start, end: at.end, replacement: '', span: span.text });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// 3. Apply them once, left to right. Two edits that share bytes cannot both be right.
|
|
206
|
+
edits.sort((a, b) => a.start - b.start);
|
|
207
|
+
let out = '';
|
|
208
|
+
let at = 0;
|
|
209
|
+
for (const edit of edits) {
|
|
210
|
+
if (edit.start < at)
|
|
211
|
+
return { ok: false, reason: 'overlap', span: edit.span };
|
|
212
|
+
out += source.slice(at, edit.start) + edit.replacement;
|
|
213
|
+
at = edit.end;
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, content: out + source.slice(at) };
|
|
216
|
+
}
|
|
217
|
+
/** The file's bytes, or null when there is no such file. Any other failure throws to the caller's catch. */
|
|
218
|
+
async function readIfPresent(file) {
|
|
219
|
+
try {
|
|
220
|
+
return await fs.readFile(file);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (err.code === 'ENOENT')
|
|
224
|
+
return null;
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Whether the change guard lets this run touch `file`; with no guard, every file may change. */
|
|
229
|
+
async function permitted(file, mayChange) {
|
|
230
|
+
return mayChange === undefined || (await mayChange(file));
|
|
231
|
+
}
|
|
232
|
+
/** `warn`, made safe to call: a closed sink must not fail the install. */
|
|
233
|
+
function quietly(warn) {
|
|
234
|
+
return msg => {
|
|
235
|
+
try {
|
|
236
|
+
warn(msg);
|
|
237
|
+
}
|
|
238
|
+
catch { /* a closed sink must not fail the install */ }
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** Write `text` unless `current` already holds exactly those bytes, atomically and executable. The caller has asked the change guard. */
|
|
242
|
+
async function writeIfChanged(file, current, text) {
|
|
243
|
+
if (current !== null && current.equals(Buffer.from(text, 'utf-8')))
|
|
244
|
+
return 'unchanged';
|
|
245
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
246
|
+
await writeFileAtomicExclusive(file, text, INSTALLED_MODE);
|
|
247
|
+
// A create mode passes through the umask, while composeScripts sets every file under hooks/ to 0755 on
|
|
248
|
+
// the next run whatever the umask is. Set the mode here too, or a second init changes it on disk.
|
|
249
|
+
if (process.platform !== 'win32')
|
|
250
|
+
await fs.chmod(file, INSTALLED_MODE);
|
|
251
|
+
return 'rewritten';
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Remove the sentinel behind the change guard, without reading it; a file that is already gone is not an
|
|
255
|
+
* error. A failure is a warning and `failed`.
|
|
256
|
+
*/
|
|
257
|
+
async function removeSentinel(sentinelTarget, mayChange, warn) {
|
|
258
|
+
try {
|
|
259
|
+
if (!(await permitted(sentinelTarget, mayChange)))
|
|
260
|
+
return 'failed';
|
|
261
|
+
await fs.unlink(sentinelTarget);
|
|
262
|
+
return 'rewritten';
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
if (err.code === 'ENOENT')
|
|
266
|
+
return 'absent';
|
|
267
|
+
warn(`charter render: could not sync the implement sentinel (${sentinelTarget}) — ${String(err)}`);
|
|
268
|
+
return 'failed';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Why a render failed, in a sentence for the warning. */
|
|
272
|
+
function describeFailure(result) {
|
|
273
|
+
const problem = {
|
|
274
|
+
missing: 'is missing from',
|
|
275
|
+
repeated: 'occurs more than once in',
|
|
276
|
+
overlap: 'overlaps another span in',
|
|
277
|
+
}[result.reason];
|
|
278
|
+
return `the span "${result.span}" ${problem} the packaged charter, so the full charter was installed.`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Write the charter rendered for `plugins`, and sync the implement sentinel with it.
|
|
282
|
+
*
|
|
283
|
+
* Called by installScripts after the script copy (which installViaFileCopy and `init --hud-only` both
|
|
284
|
+
* run), and by the selective uninstall phase. The two files are independent: each has its own try/catch,
|
|
285
|
+
* so a failure on one never costs the other. Each file is checked with the change guard before it is read.
|
|
286
|
+
* Bytes are compared with the bytes on disk at the moment of writing, so a rerun that changes nothing
|
|
287
|
+
* writes nothing.
|
|
288
|
+
*/
|
|
289
|
+
export async function installRenderedCharter(opts) {
|
|
290
|
+
const { scriptsDir: target, plugins, mayChange, mode } = opts;
|
|
291
|
+
const warn = quietly(opts.warn);
|
|
292
|
+
// Precondition, asserted in production code: a relative scriptsDir would resolve against the working directory.
|
|
293
|
+
if (!path.isAbsolute(target)) {
|
|
294
|
+
warn(`charter render: scriptsDir is not an absolute path ("${target}") — the installed charter was not rendered`);
|
|
295
|
+
return { charter: 'failed', sentinel: 'failed' };
|
|
296
|
+
}
|
|
297
|
+
const inventory = charterInventory(plugins.map(plugin => (typeof plugin === 'string' ? plugin : plugin.name)));
|
|
298
|
+
const charterTarget = path.join(target, CHARTER_RELPATH);
|
|
299
|
+
const sentinelTarget = path.join(target, HANDOFF_SENTINEL_RELPATH);
|
|
300
|
+
const packagedRoot = () => {
|
|
301
|
+
try {
|
|
302
|
+
return opts.packagedScriptsDir ?? packagedScriptsDir();
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
warn(`charter render: cannot find the packaged scripts — ${String(err)}`);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const installCharter = async () => {
|
|
310
|
+
try {
|
|
311
|
+
if (!(await permitted(charterTarget, mayChange)))
|
|
312
|
+
return 'failed';
|
|
313
|
+
const current = await readIfPresent(charterTarget);
|
|
314
|
+
if (current === null && mode === 'uninstall')
|
|
315
|
+
return 'absent';
|
|
316
|
+
const root = packagedRoot();
|
|
317
|
+
if (root === null)
|
|
318
|
+
return 'failed';
|
|
319
|
+
const source = await fs.readFile(path.join(root, CHARTER_RELPATH), 'utf-8');
|
|
320
|
+
const rendered = renderCharter(source, inventory.commands, inventory.agents);
|
|
321
|
+
if (!rendered.ok)
|
|
322
|
+
warn(`charter render: ${describeFailure(rendered)}`);
|
|
323
|
+
return await writeIfChanged(charterTarget, current, rendered.ok ? rendered.content : source);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
warn(`charter render: could not install the charter (${charterTarget}) — ${String(err)}`);
|
|
327
|
+
return 'failed';
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const syncSentinel = async () => {
|
|
331
|
+
if (!inventory.commands.has(HANDOFF_COMMAND))
|
|
332
|
+
return removeSentinel(sentinelTarget, mayChange, warn);
|
|
333
|
+
try {
|
|
334
|
+
if (!(await permitted(sentinelTarget, mayChange)))
|
|
335
|
+
return 'failed';
|
|
336
|
+
const current = await readIfPresent(sentinelTarget);
|
|
337
|
+
if (mode === 'uninstall')
|
|
338
|
+
return current === null ? 'absent' : 'unchanged';
|
|
339
|
+
const root = packagedRoot();
|
|
340
|
+
if (root === null)
|
|
341
|
+
return 'failed';
|
|
342
|
+
const text = await fs.readFile(path.join(root, HANDOFF_SENTINEL_RELPATH), 'utf-8');
|
|
343
|
+
return await writeIfChanged(sentinelTarget, current, text);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
warn(`charter render: could not sync the implement sentinel (${sentinelTarget}) — ${String(err)}`);
|
|
347
|
+
return 'failed';
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const charter = await installCharter();
|
|
351
|
+
const sentinel = await syncSentinel();
|
|
352
|
+
return { charter, sentinel };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Take the implement sentinel away when the removed plugins include the owner of the handoff command, and
|
|
356
|
+
* touch nothing else. For an uninstall with no manifest record: the charter has nothing to be rendered
|
|
357
|
+
* from and stays as it is, but the sentinel needs no record, and without it the per-prompt preamble falls
|
|
358
|
+
* back to the generic reminder instead of naming a workflow that is gone.
|
|
359
|
+
*
|
|
360
|
+
* `rewritten` when the sentinel was removed, `absent` when there was none, `unchanged` when the removed
|
|
361
|
+
* plugins leave implement installed, `failed` when the guard refused or a warning says what went wrong.
|
|
362
|
+
* Never throws.
|
|
363
|
+
*/
|
|
364
|
+
export async function removeHandoffSentinel(opts) {
|
|
365
|
+
const warn = quietly(opts.warn);
|
|
366
|
+
if (!path.isAbsolute(opts.scriptsDir)) {
|
|
367
|
+
warn(`charter render: scriptsDir is not an absolute path ("${opts.scriptsDir}") — the implement sentinel was not removed`);
|
|
368
|
+
return 'failed';
|
|
369
|
+
}
|
|
370
|
+
if (!charterInventory(opts.removedPlugins).commands.has(HANDOFF_COMMAND))
|
|
371
|
+
return 'unchanged';
|
|
372
|
+
return removeSentinel(path.join(opts.scriptsDir, HANDOFF_SENTINEL_RELPATH), opts.mayChange, warn);
|
|
373
|
+
}
|
|
374
|
+
//# sourceMappingURL=charter-render.js.map
|
|
@@ -24,7 +24,9 @@ import * as path from 'path';
|
|
|
24
24
|
import { skillsDir, rulesDir } from '../../core/assets.js';
|
|
25
25
|
import { ALWAYS_PRESENT_REFS, COMPLIANCE_FRAMEWORKS, normalizeFrameworks } from '../../core/compliance.js';
|
|
26
26
|
import { parseComplianceFragment, composeComplianceSkill, composeComplianceRule } from '../../core/compliance-compose.js';
|
|
27
|
-
import {
|
|
27
|
+
import { swapFolderIn } from './folder-swap.js';
|
|
28
|
+
import { copyDirectory, validateSkillShadow, validateRuleShadow } from './installer.js';
|
|
29
|
+
import { newLinkReport, refuseUnrealFolder, warnLinkReport, writeFileReplacingLink } from './linked-folder.js';
|
|
28
30
|
// ── Path helpers ───────────────────────────────────────────────────────────
|
|
29
31
|
/** Every registry framework id — the reference set every install carries. */
|
|
30
32
|
const ALL_FRAMEWORK_IDS = COMPLIANCE_FRAMEWORKS.map(fw => fw.id);
|
|
@@ -32,6 +34,14 @@ const ALL_FRAMEWORK_IDS = COMPLIANCE_FRAMEWORKS.map(fw => fw.id);
|
|
|
32
34
|
function skillTarget(claudeDir) {
|
|
33
35
|
return path.join(claudeDir, 'skills', 'devflow:compliance');
|
|
34
36
|
}
|
|
37
|
+
/** The folder the compliance skill is built in before it is renamed into place: {claudeDir}/skills/devflow:compliance.tmp */
|
|
38
|
+
export function complianceSkillTempPath(claudeDir) {
|
|
39
|
+
return `${skillTarget(claudeDir)}.tmp`;
|
|
40
|
+
}
|
|
41
|
+
/** Where the compliance skill folder waits while the new one is renamed in: {claudeDir}/skills/devflow:compliance.old */
|
|
42
|
+
export function complianceSkillAsidePath(claudeDir) {
|
|
43
|
+
return `${skillTarget(claudeDir)}.old`;
|
|
44
|
+
}
|
|
35
45
|
/** Installed compliance rule file: {claudeDir}/rules/devflow/compliance.md */
|
|
36
46
|
function ruleTarget(claudeDir) {
|
|
37
47
|
return path.join(claudeDir, 'rules', 'devflow', 'compliance.md');
|
|
@@ -92,13 +102,22 @@ async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
|
|
|
92
102
|
* `fragments` is loaded once by convergeComplianceArtifacts and shared with the rule
|
|
93
103
|
* installer — the SKILL.md and the rule compose from the same parsed set.
|
|
94
104
|
*
|
|
95
|
-
* Build under a .tmp sibling,
|
|
105
|
+
* Build under a .tmp sibling, then swap it in: the old target waits in a .old sibling while the new one
|
|
106
|
+
* is renamed in. A file in the old target that devflow did not ship moves to the backup folder first;
|
|
107
|
+
* if one cannot move, nothing is swapped and the shipped files are written into the old target (see
|
|
108
|
+
* {@link swapFolderIn}). An old target that is a link is moved aside as the link itself, never
|
|
109
|
+
* followed, so what it points to is left as it was; the link is recorded in `links`.
|
|
96
110
|
* Unexpected I/O failures are reported via warn; never thrown.
|
|
97
111
|
*/
|
|
98
|
-
async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, warn) {
|
|
112
|
+
async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, warn, links, backups) {
|
|
99
113
|
const canonicalSrc = path.join(skillsDir(), 'compliance');
|
|
100
114
|
const target = skillTarget(claudeDir);
|
|
101
|
-
const tmpTarget =
|
|
115
|
+
const tmpTarget = complianceSkillTempPath(claudeDir);
|
|
116
|
+
// A skills folder that is a link or not a folder is refused before anything is built: the swap
|
|
117
|
+
// would move and remove below it, in the folder the link points to. The refusal is recorded in
|
|
118
|
+
// `links`, and the converge is not clean.
|
|
119
|
+
if (await refuseUnrealFolder(links, path.dirname(target)))
|
|
120
|
+
return;
|
|
102
121
|
// Determine SKILL.md source: prefer a valid shadow, else use canonical.
|
|
103
122
|
const skillShadowDir = path.join(devflowDir, 'skills', 'compliance');
|
|
104
123
|
const shadowState = await validateSkillShadow(skillShadowDir);
|
|
@@ -148,9 +167,18 @@ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments
|
|
|
148
167
|
warn(`compliance: reference "${fw}.md" not installed — ${String(err)}`);
|
|
149
168
|
}
|
|
150
169
|
}
|
|
151
|
-
// Swap:
|
|
152
|
-
|
|
153
|
-
await
|
|
170
|
+
// Swap: the old target waits aside, the tmp folder is renamed into place. The link is recorded
|
|
171
|
+
// once the real folder stands in its place, so the report matches its wording.
|
|
172
|
+
await swapFolderIn({
|
|
173
|
+
backups,
|
|
174
|
+
target,
|
|
175
|
+
staged: tmpTarget,
|
|
176
|
+
aside: complianceSkillAsidePath(claudeDir),
|
|
177
|
+
links,
|
|
178
|
+
mergeInto: () => copyDirectory(tmpTarget, target, undefined, links),
|
|
179
|
+
});
|
|
180
|
+
// The tmp folder is gone after a swap; a merge leaves it behind.
|
|
181
|
+
await fs.rm(tmpTarget, { recursive: true, force: true });
|
|
154
182
|
}
|
|
155
183
|
catch (err) {
|
|
156
184
|
// Clean up tmp on failure (best-effort) so no orphan is left behind.
|
|
@@ -174,9 +202,11 @@ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments
|
|
|
174
202
|
* source (never the shadow — fragments are registry-owned) and shared with the skill
|
|
175
203
|
* installer.
|
|
176
204
|
*
|
|
177
|
-
* I/O failures are reported via warn; never thrown.
|
|
205
|
+
* I/O failures are reported via warn; never thrown. The caller has proved that `rules/devflow` is a
|
|
206
|
+
* real folder or absent. A link at the rule file is replaced by the rename of the atomic write,
|
|
207
|
+
* never followed, and recorded in `links`.
|
|
178
208
|
*/
|
|
179
|
-
async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, warn) {
|
|
209
|
+
async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, warn, links) {
|
|
180
210
|
const ruleShadowFile = path.join(devflowDir, 'rules', 'compliance.md');
|
|
181
211
|
const canonicalRuleSrc = path.join(rulesDir(), 'compliance.md');
|
|
182
212
|
const target = ruleTarget(claudeDir);
|
|
@@ -188,7 +218,7 @@ async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, war
|
|
|
188
218
|
for (const w of warnings)
|
|
189
219
|
warn(`compliance: ${w}`);
|
|
190
220
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
191
|
-
await
|
|
221
|
+
await writeFileReplacingLink(target, composed, links);
|
|
192
222
|
}
|
|
193
223
|
catch (err) {
|
|
194
224
|
warn(`compliance: rule install failed — ${String(err)}`);
|
|
@@ -213,13 +243,13 @@ async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, war
|
|
|
213
243
|
* principle in engineering.md.
|
|
214
244
|
*/
|
|
215
245
|
export async function convergeComplianceArtifacts(opts) {
|
|
216
|
-
const { claudeDir, devflowDir, enabled, frameworks, rulesEnabled, warn } = opts;
|
|
246
|
+
const { claudeDir, devflowDir, enabled, frameworks, rulesEnabled, warn, backups } = opts;
|
|
217
247
|
// S78: claudeDir precondition — reliability rule: assert invariants in production code.
|
|
218
248
|
// An empty or relative claudeDir would make skillTarget/ruleTarget resolve to unexpected
|
|
219
249
|
// locations. Warn and bail rather than running fs.rm with a potentially wrong path.
|
|
220
250
|
if (!path.isAbsolute(claudeDir)) {
|
|
221
251
|
warn(`compliance: claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
|
|
222
|
-
return { removedPreexisting: false, converged: false };
|
|
252
|
+
return { removedPreexisting: false, converged: false, replacedLinks: [], refusedFolders: [] };
|
|
223
253
|
}
|
|
224
254
|
// Trust boundary. `frameworks` reaches here straight from the manifest on every
|
|
225
255
|
// caller's bare --enable path, and normalizeComplianceFeature only type-checks it.
|
|
@@ -244,10 +274,24 @@ export async function convergeComplianceArtifacts(opts) {
|
|
|
244
274
|
// The skill and the rule are independent operations. An error in
|
|
245
275
|
// installSkillDir is caught internally and reported via trackingWarn, so
|
|
246
276
|
// execution always continues to the rule step.
|
|
247
|
-
|
|
277
|
+
const links = newLinkReport();
|
|
278
|
+
await installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, trackingWarn, links, backups);
|
|
279
|
+
// The rule folder is proven before the rule is written or removed: a link there sends both into
|
|
280
|
+
// another place. The skill folder is swapped whole above, and a swap never follows a link.
|
|
281
|
+
const finish = (removedPreexisting) => {
|
|
282
|
+
warnLinkReport(links, warn);
|
|
283
|
+
return {
|
|
284
|
+
removedPreexisting,
|
|
285
|
+
converged: converged && links.refusedFolders.length === 0,
|
|
286
|
+
replacedLinks: links.replacedLinks,
|
|
287
|
+
refusedFolders: links.refusedFolders,
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
if (await refuseUnrealFolder(links, path.dirname(ruleTarget(claudeDir))))
|
|
291
|
+
return finish(false);
|
|
248
292
|
if (enabled && rulesEnabled) {
|
|
249
|
-
await installRuleFile(claudeDir, devflowDir, safeFrameworks, fragments, trackingWarn);
|
|
250
|
-
return
|
|
293
|
+
await installRuleFile(claudeDir, devflowDir, safeFrameworks, fragments, trackingWarn, links);
|
|
294
|
+
return finish(false);
|
|
251
295
|
}
|
|
252
296
|
// Compliance off, or rules off: no rule. Probe first so a disable convergence
|
|
253
297
|
// can report that it removed one; absence is already the desired end state.
|
|
@@ -260,7 +304,7 @@ export async function convergeComplianceArtifacts(opts) {
|
|
|
260
304
|
trackingWarn(`compliance: failed to remove rule — ${String(err)}`);
|
|
261
305
|
}
|
|
262
306
|
}
|
|
263
|
-
return
|
|
307
|
+
return finish(!enabled && ruleExisted);
|
|
264
308
|
}
|
|
265
309
|
// ── Manifest-slice wrapper ─────────────────────────────────────────────────
|
|
266
310
|
/**
|
|
@@ -278,7 +322,7 @@ export async function convergeComplianceArtifacts(opts) {
|
|
|
278
322
|
* than the value stored in the manifest.
|
|
279
323
|
*/
|
|
280
324
|
export async function convergeFromManifest(opts) {
|
|
281
|
-
const { claudeDir, devflowDir, manifest, warn, rulesEnabledOverride } = opts;
|
|
325
|
+
const { claudeDir, devflowDir, manifest, warn, rulesEnabledOverride, backups } = opts;
|
|
282
326
|
return convergeComplianceArtifacts({
|
|
283
327
|
claudeDir,
|
|
284
328
|
devflowDir,
|
|
@@ -286,6 +330,7 @@ export async function convergeFromManifest(opts) {
|
|
|
286
330
|
frameworks: manifest.features.compliance.frameworks,
|
|
287
331
|
rulesEnabled: rulesEnabledOverride ?? manifest.features.rules,
|
|
288
332
|
warn,
|
|
333
|
+
backups,
|
|
289
334
|
});
|
|
290
335
|
}
|
|
291
336
|
//# sourceMappingURL=compliance-install.js.map
|