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
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* devflow compliance — Enable, disable, set, and check status of the compliance feature.
|
|
3
3
|
*
|
|
4
4
|
* CLI-layer module; pure helpers in src/core/compliance.ts,
|
|
5
|
-
* I/O orchestration in src/targets/claude-code/
|
|
5
|
+
* I/O orchestration in src/targets/claude-code/curation-install.ts (the one converge
|
|
6
|
+
* for both axes) and compliance-install.ts (the compliance artifacts it composes).
|
|
6
7
|
* Compliance is manifest-group (like proxy), not config.json-gated.
|
|
7
8
|
* Per-artifact failures are warn-not-throw, so one failed artifact never aborts the rest.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Every change of the switch (enable, disable, --set, interactive enable) writes the switch,
|
|
10
|
+
* runs the one converge, and only after a clean converge records which variants are installed.
|
|
11
|
+
* With compliance off the command fails closed (AC-28): it exits non-zero when an installed
|
|
12
|
+
* prompt may still post, and prints no success line.
|
|
10
13
|
* The evidence-policy lines (--status, and the --enable/--set suggestion) come
|
|
11
14
|
* from src/core/evidence-policy.ts, the seam onto the package's own resolvers;
|
|
12
15
|
* the CLI prints the keys to add to .devflow/project.json and never writes it
|
|
@@ -18,10 +21,14 @@ import * as path from 'path';
|
|
|
18
21
|
import * as p from '@clack/prompts';
|
|
19
22
|
import color from 'picocolors';
|
|
20
23
|
import { COMPLIANCE_FRAMEWORKS, normalizeFrameworks, parseFrameworkList, } from '../../core/compliance.js';
|
|
24
|
+
import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
|
|
21
25
|
import { frameworkChoices, FRAMEWORK_SELECT_MESSAGE } from './compliance-prompts.js';
|
|
22
26
|
import { COMPLIANCE_SKILL_TOKENS } from '../../core/compliance-compose.js';
|
|
23
27
|
import { evidencePolicyStatusLine, evidencePolicySuggestion, formatEvidencePolicyUnavailable, loadEvidencePolicyModule, loadSettingsModule, repoComplianceStatusLines, } from '../../core/evidence-policy.js';
|
|
28
|
+
import { postingStatus, readInstalledVariants } from '../../core/installed-variants.js';
|
|
29
|
+
import { withManifestLock } from '../../core/manifest-lock.js';
|
|
24
30
|
import { readManifest, writeManifest } from '../../core/manifest.js';
|
|
31
|
+
import { applyVariantToggle, describePostingLeft, describeVariantConverge, readRunningVersion, } from '../../targets/claude-code/curation-install.js';
|
|
25
32
|
import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
|
|
26
33
|
import { validateRuleShadow, validateSkillShadow } from '../../targets/claude-code/installer.js';
|
|
27
34
|
import { getClaudeDirectory, getDevFlowDirectory, } from '../../targets/claude-code/claude-paths.js';
|
|
@@ -67,7 +74,7 @@ export function resolveComplianceCliAction(current, action, setFrameworks) {
|
|
|
67
74
|
messages: [
|
|
68
75
|
{
|
|
69
76
|
level: 'success',
|
|
70
|
-
text: 'Compliance disabled — rule removed, frameworks remembered for re-enable',
|
|
77
|
+
text: 'Compliance disabled — the rule and every posting step removed, frameworks remembered for re-enable',
|
|
71
78
|
},
|
|
72
79
|
],
|
|
73
80
|
};
|
|
@@ -118,10 +125,12 @@ async function skillInstalled(claudeDir) {
|
|
|
118
125
|
return false;
|
|
119
126
|
}
|
|
120
127
|
}
|
|
128
|
+
/** The installed compliance rule file. */
|
|
129
|
+
const complianceRuleFile = (claudeDir) => path.join(claudeDir, 'rules', 'devflow', 'compliance.md');
|
|
121
130
|
/** Returns true if the compliance rule file exists at the install target. */
|
|
122
131
|
async function ruleInstalled(claudeDir) {
|
|
123
132
|
try {
|
|
124
|
-
await fs.access(
|
|
133
|
+
await fs.access(complianceRuleFile(claudeDir));
|
|
125
134
|
return true;
|
|
126
135
|
}
|
|
127
136
|
catch {
|
|
@@ -156,10 +165,153 @@ async function skillShadowState(devflowDir) {
|
|
|
156
165
|
return 'none';
|
|
157
166
|
}
|
|
158
167
|
}
|
|
168
|
+
/** The `Posting:` line of `--status`: the install record read against the machine's compliance switch (AC-31). */
|
|
169
|
+
export function postingStatusLine(record, complianceEnabled) {
|
|
170
|
+
const status = postingStatus(readInstalledVariants(record), complianceEnabled);
|
|
171
|
+
const shown = status === 'installed' ? color.green(status) : status === 'not installed' ? color.dim(status) : color.yellow(status);
|
|
172
|
+
return `Posting: ${shown}`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Write the switch under the manifest lock, into the manifest read inside the lock, so a field
|
|
176
|
+
* that another devflow run wrote since this command first read the manifest is kept. A lock that
|
|
177
|
+
* cannot be taken, or is lost before the write, writes nothing. Null when the switch is on disk,
|
|
178
|
+
* otherwise the reason it is not. Never throws.
|
|
179
|
+
*/
|
|
180
|
+
async function recordComplianceSwitch(devflowDir, nextState) {
|
|
181
|
+
try {
|
|
182
|
+
const locked = await withManifestLock(devflowDir, {}, async (lock) => {
|
|
183
|
+
const current = await readManifest(devflowDir);
|
|
184
|
+
if (current === null)
|
|
185
|
+
return 'no readable install manifest';
|
|
186
|
+
if (!(await lock.stillHeld()))
|
|
187
|
+
return 'the install manifest lock was lost';
|
|
188
|
+
await writeManifest(devflowDir, {
|
|
189
|
+
...current,
|
|
190
|
+
features: { ...current.features, compliance: nextState },
|
|
191
|
+
updatedAt: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
return null;
|
|
194
|
+
});
|
|
195
|
+
return locked.ok ? locked.value : 'the install manifest lock could not be taken';
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
return String(err);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Apply one change of the compliance switch: write the switch, run the one converge for both
|
|
203
|
+
* axes, and only after a clean converge record which variants are installed (the toggle helper
|
|
204
|
+
* does that last step). Every change of the enabled value converges, `--set` and interactive enable
|
|
205
|
+
* included, and a `--set` that only changes the frameworks converges too, so the stamp follows.
|
|
206
|
+
*
|
|
207
|
+
* Fail closed on the off side: with compliance off, a converge that cannot prove every installed
|
|
208
|
+
* compliance-armed prompt holds the compliance-off file is an error that lists the files, a
|
|
209
|
+
* toggle that cannot converge at all (a version skew, or no manifest) says that posting steps remain
|
|
210
|
+
* installed until `devflow init`, and a compliance rule that is still installed is an error that
|
|
211
|
+
* names the file. None of them prints the success text. A skipped prompt converge still converges
|
|
212
|
+
* the compliance skill and rule, which do not depend on the installed version. On the enable side
|
|
213
|
+
* the skip is a warning, and a rule that is wanted and still not installed is a second warning.
|
|
214
|
+
* A reference that the converge could not refresh, a linked folder for example, is an error that
|
|
215
|
+
* names it, with exit 1, and the switch stays recorded as on.
|
|
216
|
+
*
|
|
217
|
+
* Returns a result and never throws; the caller prints the messages and sets the exit code. The
|
|
218
|
+
* record of the backup session prints once, as the change returns (an exit-1 result included),
|
|
219
|
+
* ahead of the messages.
|
|
220
|
+
*/
|
|
221
|
+
export function applyComplianceChange(opts) {
|
|
222
|
+
return runWithBackupReport(opts.backups, () => applyChange(opts));
|
|
223
|
+
}
|
|
224
|
+
async function applyChange(opts) {
|
|
225
|
+
const { claudeDir, devflowDir, manifest, action, setFrameworks, runningVersion, packageRoot, warn, backups } = opts;
|
|
226
|
+
const resolved = resolveComplianceCliAction(manifest.features.compliance, action, setFrameworks);
|
|
227
|
+
// normalizeFrameworks deduplicates (e.g. --set gdpr,GDPR → [gdpr]).
|
|
228
|
+
const nextState = {
|
|
229
|
+
enabled: resolved.nextState.enabled,
|
|
230
|
+
frameworks: normalizeFrameworks(resolved.nextState.frameworks),
|
|
231
|
+
};
|
|
232
|
+
// The switch comes first: whatever the converge does next, the machine's choice is on disk.
|
|
233
|
+
const notRecorded = await recordComplianceSwitch(devflowDir, nextState);
|
|
234
|
+
if (notRecorded !== null) {
|
|
235
|
+
return {
|
|
236
|
+
nextState: manifest.features.compliance,
|
|
237
|
+
messages: [{ level: 'error', text: `Could not record the compliance switch, so nothing was changed: ${notRecorded}` }],
|
|
238
|
+
exitCode: 1,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const outcome = await applyVariantToggle({
|
|
242
|
+
claudeDir,
|
|
243
|
+
devflowDir,
|
|
244
|
+
axis: 'compliance',
|
|
245
|
+
enabled: nextState.enabled,
|
|
246
|
+
runningVersion,
|
|
247
|
+
warn,
|
|
248
|
+
packageRoot,
|
|
249
|
+
backups,
|
|
250
|
+
});
|
|
251
|
+
if (outcome.kind === 'skipped') {
|
|
252
|
+
// The compliance skill and rule do not depend on the installed version, so they converge
|
|
253
|
+
// whatever the prompt converge did. The converge warns per item and never throws.
|
|
254
|
+
await convergeFromManifest({
|
|
255
|
+
claudeDir,
|
|
256
|
+
devflowDir,
|
|
257
|
+
manifest: { features: { compliance: nextState, rules: manifest.features.rules } },
|
|
258
|
+
warn,
|
|
259
|
+
backups,
|
|
260
|
+
});
|
|
261
|
+
if (!nextState.enabled) {
|
|
262
|
+
return { nextState, messages: [{ level: 'error', text: outcome.message }], exitCode: 1 };
|
|
263
|
+
}
|
|
264
|
+
const ruleNote = manifest.features.rules && !(await ruleInstalled(claudeDir))
|
|
265
|
+
? [{ level: 'warn', text: 'The compliance rule was not installed. Run `devflow init`.' }]
|
|
266
|
+
: [];
|
|
267
|
+
return { nextState, messages: [...resolved.messages, ...ruleNote, { level: 'warn', text: outcome.message }], exitCode: 0 };
|
|
268
|
+
}
|
|
269
|
+
const { posting } = outcome.result;
|
|
270
|
+
if (!nextState.enabled && posting.kind !== 'clear') {
|
|
271
|
+
return {
|
|
272
|
+
nextState,
|
|
273
|
+
messages: [
|
|
274
|
+
{ level: 'error', text: 'Compliance is recorded as off, but the install may still post to GitHub or a tracker.' },
|
|
275
|
+
...describePostingLeft(posting).map((text) => ({ level: 'error', text })),
|
|
276
|
+
],
|
|
277
|
+
exitCode: 1,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// A failed rule removal is only a warning inside the converge. The line that says the rule is
|
|
281
|
+
// removed prints only when it is gone.
|
|
282
|
+
if (!nextState.enabled && (await ruleInstalled(claudeDir))) {
|
|
283
|
+
return {
|
|
284
|
+
nextState,
|
|
285
|
+
messages: [{
|
|
286
|
+
level: 'error',
|
|
287
|
+
text: `Compliance is recorded as off, but the compliance rule could not be removed: ${complianceRuleFile(claudeDir)}. ` +
|
|
288
|
+
'Fix the cause in the warning above, or delete the file. Then run `devflow compliance --disable` again.',
|
|
289
|
+
}],
|
|
290
|
+
exitCode: 1,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const referenceFailures = outcome.result.references.failures;
|
|
294
|
+
if (nextState.enabled && referenceFailures.length > 0) {
|
|
295
|
+
return {
|
|
296
|
+
nextState,
|
|
297
|
+
messages: [{
|
|
298
|
+
level: 'error',
|
|
299
|
+
text: 'Compliance is recorded as on, but some installed references were not refreshed: ' +
|
|
300
|
+
`${referenceFailures.join('; ')}. Replace any link in the cause with a real folder. Then run the command again.`,
|
|
301
|
+
}],
|
|
302
|
+
exitCode: 1,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const messages = [...resolved.messages];
|
|
306
|
+
const changed = describeVariantConverge(outcome.result, 'compliance', nextState.enabled);
|
|
307
|
+
if (changed !== null)
|
|
308
|
+
messages.push({ level: 'info', text: `Installed prompts: ${changed}` });
|
|
309
|
+
return { nextState, messages, exitCode: 0 };
|
|
310
|
+
}
|
|
159
311
|
export const complianceCommand = new Command('compliance')
|
|
160
312
|
.description('Enable, disable, or configure the compliance feature')
|
|
161
|
-
.option('--enable', 'Enable compliance (restores previously selected frameworks)')
|
|
162
|
-
.option('--disable', 'Disable compliance (rule removed; frameworks remembered for re-enable)')
|
|
313
|
+
.option('--enable', 'Enable compliance (restores previously selected frameworks and installs the steps that post evidence and summaries to GitHub or a tracker)')
|
|
314
|
+
.option('--disable', 'Disable compliance (rule and every step that posts evidence or summaries to GitHub or a tracker removed; frameworks remembered for re-enable)')
|
|
163
315
|
.option('--status', 'Show compliance state: manifest, installed artifacts, shadow presence, and the evidence policy for the current repository')
|
|
164
316
|
.option('--set <list>', 'Set active frameworks (comma-separated IDs); enables compliance. Use --set "" for zero frameworks (generic controls only)')
|
|
165
317
|
.action(async (options) => {
|
|
@@ -169,8 +321,8 @@ export const complianceCommand = new Command('compliance')
|
|
|
169
321
|
if (!hasFlag) {
|
|
170
322
|
p.intro(color.bgCyan(color.white(' Compliance ')));
|
|
171
323
|
const validIds = COMPLIANCE_FRAMEWORKS.map(fw => `${fw.id} — ${fw.hint}`).join('\n ');
|
|
172
|
-
p.note(`${color.cyan('devflow compliance --enable')} Enable compliance (restores prior frameworks)\n` +
|
|
173
|
-
`${color.cyan('devflow compliance --disable')} Disable compliance (frameworks remembered)\n` +
|
|
324
|
+
p.note(`${color.cyan('devflow compliance --enable')} Enable compliance (restores prior frameworks; installs the posting steps)\n` +
|
|
325
|
+
`${color.cyan('devflow compliance --disable')} Disable compliance (removes the posting steps; frameworks remembered)\n` +
|
|
174
326
|
`${color.cyan('devflow compliance --status')} Show current state and installed artifacts\n` +
|
|
175
327
|
`${color.cyan('devflow compliance --set <ids>')} Set active frameworks and enable\n\n` +
|
|
176
328
|
`Valid framework IDs:\n ${validIds}`, 'Usage');
|
|
@@ -228,6 +380,7 @@ export const complianceCommand = new Command('compliance')
|
|
|
228
380
|
? color.yellow(' (withheld — rules disabled)')
|
|
229
381
|
: '') +
|
|
230
382
|
(isRuleShadowed ? color.green(' [shadowed]') : ''),
|
|
383
|
+
postingStatusLine(manifest.installedVariants, current.enabled),
|
|
231
384
|
'',
|
|
232
385
|
// The repository in cwd, resolved by the package's own resolver with the
|
|
233
386
|
// compliance state already read above (D-POLICY-CJS-SEAM). Bounded: at most
|
|
@@ -277,23 +430,23 @@ export const complianceCommand = new Command('compliance')
|
|
|
277
430
|
else {
|
|
278
431
|
action = 'disable';
|
|
279
432
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
433
|
+
// The session comes before the first change. Without a shipped set the command changes nothing.
|
|
434
|
+
const backups = await openBackupSessionOrFail({ claudeDir, devflowDir });
|
|
435
|
+
if (backups === null)
|
|
436
|
+
return;
|
|
437
|
+
// Write the switch, converge both axes, record the installed variants (applyComplianceChange).
|
|
438
|
+
const applied = await applyComplianceChange({
|
|
283
439
|
claudeDir,
|
|
284
440
|
devflowDir,
|
|
285
|
-
manifest
|
|
441
|
+
manifest,
|
|
442
|
+
action,
|
|
443
|
+
setFrameworks,
|
|
444
|
+
runningVersion: await readRunningVersion(),
|
|
286
445
|
warn: (msg) => p.log.warn(msg),
|
|
446
|
+
backups,
|
|
287
447
|
});
|
|
288
|
-
// Persist to manifest — normalizeFrameworks deduplicates (e.g. --set gdpr,GDPR → [gdpr])
|
|
289
|
-
manifest.features.compliance = {
|
|
290
|
-
enabled: resolved.nextState.enabled,
|
|
291
|
-
frameworks: normalizeFrameworks(resolved.nextState.frameworks),
|
|
292
|
-
};
|
|
293
|
-
manifest.updatedAt = new Date().toISOString();
|
|
294
|
-
await writeManifest(devflowDir, manifest);
|
|
295
448
|
// Emit messages
|
|
296
|
-
for (const msg of
|
|
449
|
+
for (const msg of applied.messages) {
|
|
297
450
|
switch (msg.level) {
|
|
298
451
|
case 'success':
|
|
299
452
|
p.log.success(msg.text);
|
|
@@ -301,18 +454,25 @@ export const complianceCommand = new Command('compliance')
|
|
|
301
454
|
case 'warn':
|
|
302
455
|
p.log.warn(msg.text);
|
|
303
456
|
break;
|
|
457
|
+
case 'error':
|
|
458
|
+
p.log.error(msg.text);
|
|
459
|
+
break;
|
|
304
460
|
default:
|
|
305
461
|
p.log.info(msg.text);
|
|
306
462
|
break;
|
|
307
463
|
}
|
|
308
464
|
}
|
|
309
|
-
if (
|
|
465
|
+
if (applied.exitCode !== 0) {
|
|
466
|
+
process.exitCode = applied.exitCode;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (applied.nextState.enabled && !manifest.features.rules) {
|
|
310
470
|
p.log.info(color.dim('Note: compliance rule withheld (rules disabled) — ' +
|
|
311
471
|
'run `devflow rules --enable` to install the stamped rule'));
|
|
312
472
|
}
|
|
313
473
|
// Suggest the team file compliance now implies. Printed, never written:
|
|
314
474
|
// .devflow/project.json is team-owned (D-POLICY-NO-WRITE).
|
|
315
|
-
if (
|
|
475
|
+
if (applied.nextState.enabled) {
|
|
316
476
|
const policyModule = loadEvidencePolicyModule();
|
|
317
477
|
const settingsModule = loadSettingsModule();
|
|
318
478
|
if (!policyModule.ok) {
|
|
@@ -322,7 +482,7 @@ export const complianceCommand = new Command('compliance')
|
|
|
322
482
|
p.log.warn(formatEvidencePolicyUnavailable(settingsModule.error));
|
|
323
483
|
}
|
|
324
484
|
else {
|
|
325
|
-
const suggestion = evidencePolicySuggestion(
|
|
485
|
+
const suggestion = evidencePolicySuggestion(applied.nextState, policyModule.value, settingsModule.value);
|
|
326
486
|
if (suggestion !== null)
|
|
327
487
|
p.note(suggestion, 'Evidence policy');
|
|
328
488
|
}
|
|
@@ -9,7 +9,8 @@ import { loadShippedAgentDefaults } from '../../core/agent-models.js';
|
|
|
9
9
|
import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
|
|
10
10
|
import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
|
|
11
11
|
import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
12
|
-
import {
|
|
12
|
+
import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
|
|
13
|
+
import { applyVariantToggle, describeCurationConverge, describePostingLeft, installedScriptsSkew, readRunningVersion, } from '../../targets/claude-code/curation-install.js';
|
|
13
14
|
import { getLedgerRoot } from '../../core/ledger-root.js';
|
|
14
15
|
import { AREA_PAUSE_NOTICE } from '../../core/area-pause-notice.js';
|
|
15
16
|
import { probeTrackedArea } from '../../core/tracked-areas.js';
|
|
@@ -643,27 +644,33 @@ async function handleClear() {
|
|
|
643
644
|
/**
|
|
644
645
|
* Make the installed prompts match the switch just written
|
|
645
646
|
* (D-LEARNING-VARIANT-INSTALL): the curation-on or curation-off commands and
|
|
646
|
-
* agents, and the apply-decisions skill. The
|
|
647
|
-
*
|
|
648
|
-
* overrides survive.
|
|
647
|
+
* agents, and the apply-decisions skill. The one converge for both axes that
|
|
648
|
+
* `devflow init` and `devflow compliance` run, followed by reapplying the saved
|
|
649
|
+
* agent model mapping so `devflow agents` overrides survive.
|
|
649
650
|
*
|
|
650
651
|
* Warn-not-fail: the switch itself is already recorded, and either mismatch is
|
|
651
652
|
* safe (an on variant is still gated at run time, an off variant loads no
|
|
652
|
-
* decisions), so a problem here never changes the exit code.
|
|
653
|
+
* decisions), so a problem here never changes the exit code. A compliance-off
|
|
654
|
+
* machine that still has a prompt that posts is a warning here too: the fail-closed
|
|
655
|
+
* exit belongs to `devflow compliance`, which changes that axis.
|
|
653
656
|
*
|
|
654
657
|
* Answers whether the converge ran, which means the installed files are the ones
|
|
655
658
|
* this CLI writes. The toggle moves the ledger folder only then: under version skew
|
|
656
659
|
* (or when that could not be told) it moves nothing.
|
|
657
660
|
*/
|
|
658
|
-
async function convergeInstalledVariants(enabled) {
|
|
661
|
+
export async function convergeInstalledVariants(enabled, backups) {
|
|
659
662
|
try {
|
|
660
|
-
|
|
663
|
+
// The record of the session prints when the toggle returns or throws, ahead of the warning
|
|
664
|
+
// below, so a file moved aside before a failure is still named.
|
|
665
|
+
const outcome = await runWithBackupReport(backups, async () => applyVariantToggle({
|
|
661
666
|
claudeDir: getClaudeDirectory(),
|
|
662
667
|
devflowDir: getDevFlowDirectory(),
|
|
663
|
-
|
|
668
|
+
axis: 'curation',
|
|
669
|
+
enabled,
|
|
664
670
|
runningVersion: await readRunningVersion(),
|
|
665
671
|
warn: (msg) => p.log.warn(msg),
|
|
666
|
-
|
|
672
|
+
backups,
|
|
673
|
+
}));
|
|
667
674
|
if (outcome.kind === 'skipped') {
|
|
668
675
|
p.log.warn(outcome.message);
|
|
669
676
|
return 'not-converged';
|
|
@@ -671,6 +678,8 @@ async function convergeInstalledVariants(enabled) {
|
|
|
671
678
|
const changed = describeCurationConverge(outcome.result, enabled);
|
|
672
679
|
if (changed !== null)
|
|
673
680
|
p.log.info(color.dim(`Installed prompts: ${changed}`));
|
|
681
|
+
for (const line of describePostingLeft(outcome.result.posting))
|
|
682
|
+
p.log.warn(line);
|
|
674
683
|
return 'converged';
|
|
675
684
|
}
|
|
676
685
|
catch (err) {
|
|
@@ -716,13 +725,17 @@ async function tryMoveForToggle(ledgerRoot) {
|
|
|
716
725
|
* tries the shared move with no wait. Only `--disable` drains, skew included.
|
|
717
726
|
*/
|
|
718
727
|
async function handleToggle(enabled) {
|
|
728
|
+
// The session comes before the first change. Without a shipped set the toggle changes nothing.
|
|
729
|
+
const backups = await openBackupSessionOrFail({ claudeDir: getClaudeDirectory(), devflowDir: getDevFlowDirectory() });
|
|
730
|
+
if (backups === null)
|
|
731
|
+
return;
|
|
719
732
|
const recorded = await writeMachineFeature(getDevFlowDirectory(), 'curation', enabled);
|
|
720
733
|
if (!recorded.ok) {
|
|
721
734
|
p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
|
|
722
735
|
process.exitCode = 1;
|
|
723
736
|
return;
|
|
724
737
|
}
|
|
725
|
-
const converged = await convergeInstalledVariants(enabled);
|
|
738
|
+
const converged = await convergeInstalledVariants(enabled, backups);
|
|
726
739
|
const ledgerRoot = await getLedgerRoot();
|
|
727
740
|
const shownSkip = converged === 'converged' && ledgerRoot ? await tryMoveForToggle(ledgerRoot) : null;
|
|
728
741
|
if (enabled) {
|
|
@@ -31,23 +31,18 @@ import { DEFAULT_GATE_LEVEL, GATE_LEVELS, MALFORMED_GATE_LEVEL_READS_AS, parseGa
|
|
|
31
31
|
/** Message shown on the gate level select prompt. */
|
|
32
32
|
export const GATE_LEVEL_SELECT_MESSAGE = 'Gate level for this machine';
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* level
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* focuses and the levels that skip Polish). The clauses that lean skips Harden on a
|
|
42
|
-
* docs-only diff, that standard and lean run one Evaluate pass and that standard and lean
|
|
43
|
-
* apply severity tiers in /resolve have no facet row in `tests/gate-level-prompts.test.ts`:
|
|
44
|
-
* the docs guard checks each at every doc site, against its row in the /implement or
|
|
45
|
-
* /resolve level reference.
|
|
34
|
+
* One short line for each level, as the select hint and the catalogue show it.
|
|
35
|
+
* clack never wraps a note or a hint, so a hint stays within 60 characters and holds no line
|
|
36
|
+
* break (a guard in `tests/gate-level-prompts.test.ts` holds both). The full effects of each
|
|
37
|
+
* level live in GATE_LEVEL_EFFECT in gates.ts, in `devflow gates` and in the docs.
|
|
38
|
+
* Change a hint when the level it names changes how much it checks; then check
|
|
39
|
+
* GATE_LEVEL_EFFECT and update the pin. The comment on GATE_LEVEL_EFFECT holds the full
|
|
40
|
+
* update rule, the pin and the list of clauses that the two tables share.
|
|
46
41
|
*/
|
|
47
42
|
export const GATE_LEVEL_HINTS = Object.freeze({
|
|
48
|
-
full: 'the most checking: every review focus,
|
|
49
|
-
standard: 'fewer review focuses
|
|
50
|
-
lean: 'the least checking
|
|
43
|
+
full: 'the most checking: every review focus, and Polish',
|
|
44
|
+
standard: 'fewer review focuses, no Polish; the default',
|
|
45
|
+
lean: 'the least checking: no Polish, Validate runs the lint pack',
|
|
51
46
|
});
|
|
52
47
|
/** Build clack select options for the gate level list, most to least checking. */
|
|
53
48
|
export function gateLevelChoices() {
|
|
@@ -55,12 +50,24 @@ export function gateLevelChoices() {
|
|
|
55
50
|
}
|
|
56
51
|
/**
|
|
57
52
|
* Format a padded level catalogue suitable for a clack note body.
|
|
58
|
-
* Produces: ` standard fewer review focuses
|
|
53
|
+
* Produces: ` standard fewer review focuses, no Polish; the default`
|
|
59
54
|
*/
|
|
60
55
|
export function formatGateLevelCatalogue() {
|
|
61
56
|
const width = Math.max(...GATE_LEVELS.map(level => level.length));
|
|
62
57
|
return 'Levels:\n' + GATE_LEVELS.map(level => ` ${level.padEnd(width)} ${GATE_LEVEL_HINTS[level]}`).join('\n');
|
|
63
58
|
}
|
|
59
|
+
// ── Where the stored level was found ───────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Where init found the stored machine gate level:
|
|
62
|
+
*
|
|
63
|
+
* manifest in the manifest init reads (or nowhere, on a first install or after
|
|
64
|
+
* --reset): the seed holds it
|
|
65
|
+
* rejected in a manifest that readManifest() refuses, read raw: init keeps the
|
|
66
|
+
* level as stored while it replaces that manifest
|
|
67
|
+
* unreadable nowhere: the manifest cannot be read at all, or stores a level that cannot be
|
|
68
|
+
* written back as JSON, so what it stored is unknown
|
|
69
|
+
*/
|
|
70
|
+
export const STORED_GATE_LEVEL_ORIGINS = Object.freeze(['manifest', 'rejected', 'unreadable']);
|
|
64
71
|
/** `features.gateLevel` of a raw manifest, exactly as stored; `undefined` for an absent key. */
|
|
65
72
|
function rawGateLevelOf(raw) {
|
|
66
73
|
const features = raw.features;
|
|
@@ -228,17 +235,24 @@ export function describeGateLevel(stored) {
|
|
|
228
235
|
export function gateLevelPromptSeed(stored, origin = 'manifest') {
|
|
229
236
|
return appliedLevel(classifyGateLevel(stored, origin));
|
|
230
237
|
}
|
|
231
|
-
/** The "Current setting" lines of the prompt note, from the stored value and where it was found. */
|
|
238
|
+
/** The "Current setting" lines of the prompt note, from the stored value and where it was found. Each line fits the 72 columns the note allows. */
|
|
232
239
|
function currentSettingLines(stored, origin) {
|
|
233
240
|
switch (origin) {
|
|
234
241
|
case 'manifest':
|
|
235
242
|
return `Current setting: ${describeGateLevel(stored)}`;
|
|
236
243
|
case 'rejected':
|
|
237
|
-
return
|
|
238
|
-
|
|
244
|
+
return [
|
|
245
|
+
`Current setting: ${describeGateLevel(stored)}`,
|
|
246
|
+
'It comes from a manifest that devflow rejects.',
|
|
247
|
+
'Init replaces that manifest and keeps this level',
|
|
248
|
+
'unless you choose another.',
|
|
249
|
+
].join('\n');
|
|
239
250
|
case 'unreadable':
|
|
240
|
-
return
|
|
241
|
-
|
|
251
|
+
return [
|
|
252
|
+
'Current setting: unknown',
|
|
253
|
+
`The stored gate level cannot be read, so ${DEFAULT_GATE_LEVEL} applies`,
|
|
254
|
+
'until you choose a level.',
|
|
255
|
+
].join('\n');
|
|
242
256
|
default: {
|
|
243
257
|
const exhaustive = origin;
|
|
244
258
|
return exhaustive;
|
|
@@ -320,10 +334,10 @@ export function buildClackGateLevelPrompts() {
|
|
|
320
334
|
export async function runGateLevelStep(opts) {
|
|
321
335
|
const { stored, storedOrigin = 'manifest', prompts } = opts;
|
|
322
336
|
prompts.note(`${currentSettingLines(stored, storedOrigin)}\n\n` +
|
|
323
|
-
'Sets how much checking /code-review, /plan
|
|
324
|
-
'machine. A personal "gateLevel" in a repository\'s
|
|
325
|
-
'overrides it in that repository. Change it later
|
|
326
|
-
'devflow gates --set <level>.\n\n' +
|
|
337
|
+
'Sets how much checking /code-review, /plan, /implement and /resolve\n' +
|
|
338
|
+
'apply on this machine. A personal "gateLevel" in a repository\'s\n' +
|
|
339
|
+
'.devflow/config.json overrides it in that repository. Change it later\n' +
|
|
340
|
+
'with devflow gates --set <level>.\n\n' +
|
|
327
341
|
formatGateLevelCatalogue(), 'Gate level');
|
|
328
342
|
const outcome = await prompts.selectLevel({
|
|
329
343
|
message: GATE_LEVEL_SELECT_MESSAGE,
|
|
@@ -39,16 +39,14 @@ export const GATE_LEVEL_MEANINGS = Object.freeze({
|
|
|
39
39
|
/**
|
|
40
40
|
* Which commands read the level today; each later command that reads it updates this text.
|
|
41
41
|
*
|
|
42
|
-
* The init prompt
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* The init prompt gives each level one short line in GATE_LEVEL_HINTS (gate-level-prompts.ts);
|
|
43
|
+
* this text and the docs hold the full effects. Change the hints too when a level changes
|
|
44
|
+
* how much it checks. A command that reads the level and has no rule that changes a phase
|
|
45
|
+
* leaves the hints true, so it changes this text alone.
|
|
45
46
|
* One test pins the exact text of both tables, so a change to either fails it until you
|
|
46
47
|
* check the other table and update the pin; further rows check the clauses the two tables
|
|
47
48
|
* share (the lint pack level, the levels that run fewer review focuses and the levels that
|
|
48
|
-
* skip Polish).
|
|
49
|
-
* and lean run one Evaluate pass and that standard and lean apply severity tiers in /resolve
|
|
50
|
-
* have no facet row in `tests/gate-level-prompts.test.ts`: the docs guard checks each at
|
|
51
|
-
* every doc site, against its row in the /implement or /resolve level reference.
|
|
49
|
+
* skip Polish).
|
|
52
50
|
*/
|
|
53
51
|
export const GATE_LEVEL_EFFECT = '/code-review and /plan read the level: standard and lean run fewer review focuses and fewer planning agents. At lean, /implement has Validate run the project\'s lint pack. /implement and /resolve also read the level and name it in their report; at standard and lean they skip Polish, and full keeps it. At lean, /implement skips Harden on a docs-only diff. At standard and lean, /implement runs one Evaluate pass and at most one Fix round. At standard and lean, /resolve applies severity tiers: it fixes or escalates every security finding, fixes CRITICAL, HIGH and severe MEDIUM findings in the branch\'s own code, fixes prose-only MEDIUM findings with text edits alone, and sends lower findings and pre-existing non-security findings to one backlog issue.';
|
|
54
52
|
const MACHINE_MANIFEST_NAME = 'manifest.json';
|