devflow-kit 3.3.0 → 3.5.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 +47 -0
- package/README.md +8 -4
- package/dist/agents/code-edit.md +133 -0
- package/dist/agents/code-fix.md +165 -0
- package/dist/agents/code.md +187 -0
- package/{src/assets → dist}/agents/design.md +1 -2
- package/{src/assets → dist}/agents/diagnose.md +1 -2
- package/dist/agents/git.md +33 -722
- package/{src/assets → dist}/agents/knowledge.md +12 -5
- package/{src/assets → dist}/agents/research.md +8 -2
- package/{src/assets → dist}/agents/review.md +8 -11
- package/{src/assets → dist}/agents/scrutinize.md +4 -1
- package/dist/agents/skim.md +152 -0
- package/{src/assets → dist}/agents/triage.md +9 -2
- package/dist/cli/commands/behavior-prompts.js +77 -0
- package/dist/cli/commands/behavior.js +123 -0
- package/dist/cli/commands/flags.js +8 -2
- package/dist/cli/commands/init-seed.js +7 -3
- package/dist/cli/commands/init.js +134 -5
- package/dist/cli/commands/learning.js +38 -3
- package/dist/cli/commands/rules.js +16 -0
- package/dist/cli/commands/tracker.js +112 -15
- package/dist/cli/commands/uninstall.js +42 -1
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +35 -13
- package/dist/commands/code-review.md +155 -69
- package/dist/commands/debug.md +39 -16
- package/dist/commands/dynamic-build.md +61 -72
- package/dist/commands/dynamic-plan.md +39 -25
- package/dist/commands/dynamic-profile.md +28 -14
- package/dist/commands/dynamic-tickets.md +38 -22
- package/dist/commands/explore.md +18 -16
- package/dist/commands/implement.md +121 -150
- package/dist/commands/plan.md +51 -31
- package/dist/commands/release.md +72 -6
- package/dist/commands/research.md +49 -20
- package/dist/commands/resolve.md +54 -46
- package/dist/commands/self-review.md +38 -25
- package/dist/core/agent-frontmatter.js +101 -0
- package/dist/core/agent-models.js +43 -0
- package/dist/core/assets.js +55 -10
- package/dist/core/claude-md-audit.js +190 -0
- package/dist/core/compliance-compose.js +6 -4
- package/dist/core/feature-switch.js +41 -1
- package/dist/core/flags.js +139 -0
- package/dist/core/fs-atomic.js +8 -3
- package/dist/core/learning-variants.js +213 -0
- package/dist/core/manifest.js +73 -3
- package/dist/core/mds-variants.js +98 -19
- package/dist/core/plugins.js +111 -72
- package/dist/core/project-paths.js +0 -4
- package/dist/core/proxy-log.js +1 -1
- package/dist/core/proxy-state.js +1 -1
- package/dist/core/tracker.js +120 -2
- package/dist/learning-off/agents/code-edit.md +129 -0
- package/dist/learning-off/agents/code-fix.md +161 -0
- package/dist/learning-off/agents/code.md +183 -0
- package/dist/learning-off/agents/design.md +118 -0
- package/dist/learning-off/agents/diagnose.md +210 -0
- package/dist/learning-off/agents/knowledge.md +96 -0
- package/dist/learning-off/agents/research.md +155 -0
- package/dist/learning-off/agents/review.md +224 -0
- package/dist/learning-off/agents/scrutinize.md +120 -0
- package/{src/assets → dist/learning-off}/agents/skim.md +5 -8
- package/dist/learning-off/agents/triage.md +170 -0
- package/dist/learning-off/commands/bug-analysis.md +420 -0
- package/dist/learning-off/commands/code-review.md +530 -0
- package/dist/learning-off/commands/debug.md +314 -0
- package/dist/learning-off/commands/dynamic-build.md +1245 -0
- package/dist/learning-off/commands/dynamic-plan.md +426 -0
- package/dist/learning-off/commands/dynamic-profile.md +214 -0
- package/dist/learning-off/commands/dynamic-tickets.md +634 -0
- package/dist/learning-off/commands/explore.md +210 -0
- package/dist/learning-off/commands/implement.md +774 -0
- package/dist/learning-off/commands/plan.md +671 -0
- package/dist/learning-off/commands/release.md +311 -0
- package/dist/learning-off/commands/research.md +229 -0
- package/dist/learning-off/commands/resolve.md +842 -0
- package/dist/learning-off/commands/self-review.md +266 -0
- package/dist/skills/git/references/learn-conventions.md +27 -3
- package/dist/skills/git/references/pr/check-ci-status.md +19 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +19 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
- package/dist/skills/git/references/pr/post-review-summary.md +20 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
- package/dist/skills/git/references/pr/update-pr-evidence.md +13 -0
- package/dist/skills/git/references/pr/validate-branch.md +26 -0
- package/dist/skills/git/references/tracker/_contract.md +33 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/github/create-release.md +30 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/github/fetch-issue.md +38 -1
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +49 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +29 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/github/post-wave-report.md +17 -1
- package/dist/skills/git/references/tracker/github/setup-task.md +53 -1
- package/dist/skills/git/references/tracker/jira/associate-release.md +25 -1
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +38 -1
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +49 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +29 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +17 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +55 -3
- package/dist/skills/git/references/tracker/linear/associate-release.md +25 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +38 -1
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +49 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +29 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +17 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +55 -3
- package/dist/targets/claude-code/agent-type-owner.js +164 -0
- package/dist/targets/claude-code/behavior-install.js +111 -0
- package/dist/targets/claude-code/installer.js +140 -37
- package/dist/targets/claude-code/language-stamp.js +185 -0
- package/dist/targets/claude-code/learning-install.js +497 -0
- package/dist/targets/claude-code/tracker-install.js +33 -16
- package/dist/targets/claude-code/tracker-servers.js +107 -0
- package/package.json +4 -3
- package/src/assets/agents/_partials/_code.mds +109 -0
- package/src/assets/agents/code-edit.mds +81 -0
- package/src/assets/agents/code-fix.mds +113 -0
- package/src/assets/agents/code.mds +135 -0
- package/src/assets/agents/design.mds +148 -0
- package/src/assets/agents/diagnose.mds +225 -0
- package/src/assets/agents/evaluate.md +8 -4
- package/src/assets/agents/explore.md +58 -0
- package/src/assets/agents/git.mds +33 -722
- package/src/assets/agents/knowledge.mds +131 -0
- package/src/assets/agents/learning.md +19 -69
- package/src/assets/agents/research.mds +182 -0
- package/src/assets/agents/review.mds +282 -0
- package/src/assets/agents/scrutinize.mds +135 -0
- package/src/assets/agents/simplify.md +3 -0
- package/src/assets/agents/skim.mds +165 -0
- package/src/assets/agents/synthesize.md +26 -352
- package/src/assets/agents/test.md +8 -0
- package/src/assets/agents/tracker.md +20 -7
- package/src/assets/agents/triage.mds +201 -0
- package/src/assets/agents/validate.md +13 -6
- package/src/assets/commands/_partials/_compliance.mds +41 -4
- package/src/assets/commands/_partials/_decisions.mds +33 -2
- package/src/assets/commands/_partials/_engine.mds +14 -6
- package/src/assets/commands/_partials/_knowledge.mds +25 -12
- package/src/assets/commands/_partials/_preamble.mds +34 -10
- package/src/assets/commands/_partials/_publication.mds +5 -4
- package/src/assets/commands/_partials/_roster.mds +4 -3
- package/src/assets/commands/_partials/_settings.mds +13 -5
- package/src/assets/commands/_partials/_wave.mds +9 -1
- package/src/assets/commands/bug-analysis.mds +27 -5
- package/src/assets/commands/code-review.mds +158 -52
- package/src/assets/commands/debug.mds +41 -3
- package/src/assets/commands/dynamic-build.mds +47 -27
- package/src/assets/commands/dynamic-plan.mds +40 -9
- package/src/assets/commands/dynamic-profile.mds +9 -1
- package/src/assets/commands/dynamic-tickets.mds +21 -6
- package/src/assets/commands/explore.mds +28 -2
- package/src/assets/commands/implement.mds +126 -129
- package/src/assets/commands/plan.mds +58 -14
- package/src/assets/commands/{release.md → release.mds} +30 -26
- package/src/assets/commands/research.mds +42 -11
- package/src/assets/commands/resolve.mds +59 -15
- package/src/assets/commands/self-review.mds +30 -5
- package/src/assets/mds/.mdsroot +4 -0
- package/src/assets/mds/git/_ops.mds +383 -0
- package/src/assets/mds/git/_ops_pr.mds +201 -0
- package/src/assets/mds/git/_ops_steps.mds +147 -0
- package/src/assets/mds/git/_pr.mds +39 -44
- package/src/assets/mds/git/_references.mds +6 -3
- package/src/assets/mds/tracker/_common.mds +5 -5
- package/src/assets/mds/tracker/_contract.mds +73 -0
- package/src/assets/mds/tracker/_github.mds +73 -16
- package/src/assets/mds/tracker/_jira.mds +77 -19
- package/src/assets/mds/tracker/_linear.mds +77 -19
- package/src/assets/mds/tracker/_mcp.mds +2 -2
- package/src/assets/mds/tracker/_steps.mds +98 -0
- package/src/assets/rules/behavior.md +6 -0
- package/src/assets/rules/context-economy.md +10 -0
- package/src/assets/rules/go.md +1 -0
- package/src/assets/rules/java.md +1 -0
- package/src/assets/rules/python.md +1 -0
- package/src/assets/rules/rust.md +1 -0
- package/src/assets/rules/typescript.md +1 -0
- package/src/assets/scripts/claude-md-audit.cjs +611 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
- package/src/assets/scripts/hooks/json-helper.cjs +13 -5
- package/src/assets/scripts/hooks/json-parse +34 -10
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -6
- package/src/assets/scripts/hooks/session-start-context +315 -7
- package/src/assets/skills/agent-references/SKILL.md +33 -0
- package/src/assets/skills/agent-references/references/learning/maintain.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
- package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
- package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
- package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
- package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
- package/src/assets/skills/apply-decisions/SKILL.md +1 -1
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +5 -5
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +80 -83
- package/src/assets/skills/docs-framework/references/patterns.md +14 -25
- package/src/assets/skills/docs-framework/references/violations.md +7 -7
- package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
- package/src/assets/skills/gap-analysis/SKILL.md +0 -16
- package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
- package/src/assets/skills/quality-gates/SKILL.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
- package/src/assets/agents/code.md +0 -330
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's view of the CLAUDE.md import audit — a typed seam onto the package's
|
|
3
|
+
* own `claude-md-audit.cjs`, never a second implementation of it.
|
|
4
|
+
*
|
|
5
|
+
* D-CLAUDE-MD-IMPORT-AUDIT: the logic (import grammar, bounds, thresholds, display
|
|
6
|
+
* text, stamp format) lives once, in src/assets/scripts/claude-md-audit.cjs, because
|
|
7
|
+
* the SessionStart hook reaches only `~/.devflow/scripts/`, which `composeScripts`
|
|
8
|
+
* fills by copying `src/assets/scripts/` verbatim. This module is the typed facade
|
|
9
|
+
* `devflow init` calls, loaded with the evidence-policy seam's `loadScript` against
|
|
10
|
+
* an explicit surface map (D-POLICY-CJS-SEAM). The interfaces below are TRANSCRIBED
|
|
11
|
+
* from the script's JSDoc and are this side's only shape authority; the module is
|
|
12
|
+
* required from `scriptsDir()` (the package's own copy), never from the installed
|
|
13
|
+
* `~/.devflow/scripts`, which may be older than this CLI.
|
|
14
|
+
*
|
|
15
|
+
* Nothing here throws: a load failure, a script that throws and a stamp that cannot
|
|
16
|
+
* be written are `Result` errors, and init prints at most one degraded line for them
|
|
17
|
+
* and keeps its exit code.
|
|
18
|
+
*
|
|
19
|
+
* D-AUDIT-STAMP (the caller's half): the audit writes nothing; this module writes the
|
|
20
|
+
* stamp after the install, only under an existing machine root (a command never
|
|
21
|
+
* creates `~/.devflow` for it), only through a sibling temp file renamed into place
|
|
22
|
+
* ({@link writeFileAtomicExclusive}), and never through a link. Because
|
|
23
|
+
* `writeFileAtomicExclusive` does not refuse a symlink at the target, the write does
|
|
24
|
+
* its own `lstat` regular-file check first (AC-437): a stamp path that is a symbolic
|
|
25
|
+
* link or any other non-regular file is refused and nothing is written.
|
|
26
|
+
*/
|
|
27
|
+
import { promises as fs } from 'fs';
|
|
28
|
+
import { join } from 'path';
|
|
29
|
+
import { scriptsDir } from './assets.js';
|
|
30
|
+
import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
31
|
+
import { loadScript, } from './evidence-policy.js';
|
|
32
|
+
/** The script, relative to src/assets/scripts/ (and ~/.devflow/scripts/). */
|
|
33
|
+
export const CLAUDE_MD_AUDIT_SCRIPT_NAME = 'claude-md-audit.cjs';
|
|
34
|
+
/** The stamp's basename under the machine root (`~/.devflow`). */
|
|
35
|
+
export const CLAUDE_MD_AUDIT_STAMP_FILE = '.claude-md-audit';
|
|
36
|
+
/**
|
|
37
|
+
* The prefix of the stamp's sibling temp file, `<stamp>.tmp.<pid>`: the one name
|
|
38
|
+
* the hook and {@link writeFileAtomicExclusive} both create, so a crash between the
|
|
39
|
+
* write and the rename leaves a name uninstall can sweep.
|
|
40
|
+
*/
|
|
41
|
+
export const CLAUDE_MD_AUDIT_STAMP_TMP_PREFIX = `${CLAUDE_MD_AUDIT_STAMP_FILE}.tmp.`;
|
|
42
|
+
/** The stamp is created owner-only, the mode the hook's `umask 077` write gives it. */
|
|
43
|
+
const STAMP_CREATE_MODE = 0o600;
|
|
44
|
+
/** Every key of ClaudeMdAuditModule and the runtime kind the loader requires of it. */
|
|
45
|
+
export const CLAUDE_MD_AUDIT_MODULE_SURFACE = Object.freeze({
|
|
46
|
+
FILE_THRESHOLD_BYTES: 'number',
|
|
47
|
+
CHAIN_THRESHOLD_BYTES: 'number',
|
|
48
|
+
MAX_HOPS: 'number',
|
|
49
|
+
MAX_PATHS_PER_ROOT: 'number',
|
|
50
|
+
SCAN_BYTES: 'number',
|
|
51
|
+
MAX_BYTES_READ: 'number',
|
|
52
|
+
MAX_STAMP_BYTES: 'number',
|
|
53
|
+
MAX_KEYS: 'number',
|
|
54
|
+
SKIP_FILE_BYTES: 'number',
|
|
55
|
+
NOT_SHOWN: 'string',
|
|
56
|
+
HOOK_MAGIC: 'string',
|
|
57
|
+
HOOK_END: 'string',
|
|
58
|
+
extractImports: 'function',
|
|
59
|
+
isDisplayable: 'function',
|
|
60
|
+
isRecordable: 'function',
|
|
61
|
+
formatFinding: 'function',
|
|
62
|
+
findingKey: 'function',
|
|
63
|
+
parseStamp: 'function',
|
|
64
|
+
renderStamp: 'function',
|
|
65
|
+
readStampFile: 'function',
|
|
66
|
+
audit: 'function',
|
|
67
|
+
main: 'function',
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Load the audit script from `dir` (default: the package's own scripts directory)
|
|
71
|
+
* and shape-check its surface. Never throws.
|
|
72
|
+
*/
|
|
73
|
+
export function loadClaudeMdAuditModule(dir = scriptsDir()) {
|
|
74
|
+
return loadScript(join(dir, CLAUDE_MD_AUDIT_SCRIPT_NAME), CLAUDE_MD_AUDIT_MODULE_SURFACE);
|
|
75
|
+
}
|
|
76
|
+
// ── Roots ──────────────────────────────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* The gated root set, in the order the hook passes it: `CLAUDE.md` in the Claude
|
|
79
|
+
* config directory, then (only for a git project that is not HOME — the caller
|
|
80
|
+
* passes `null` otherwise) the project's `CLAUDE.md`, `.claude/CLAUDE.md` and
|
|
81
|
+
* `CLAUDE.local.md` at its toplevel. Ancestor directories, lazily loaded
|
|
82
|
+
* subdirectory files and `rules/*.md` are not roots.
|
|
83
|
+
*/
|
|
84
|
+
export function claudeMdAuditRoots(claudeDir, projectRoot) {
|
|
85
|
+
const roots = [join(claudeDir, 'CLAUDE.md')];
|
|
86
|
+
if (projectRoot !== null) {
|
|
87
|
+
roots.push(join(projectRoot, 'CLAUDE.md'), join(projectRoot, '.claude', 'CLAUDE.md'), join(projectRoot, 'CLAUDE.local.md'));
|
|
88
|
+
}
|
|
89
|
+
return roots;
|
|
90
|
+
}
|
|
91
|
+
/** The stamp's text when it is a regular file of at most MAX_STAMP_BYTES, else null (read as absent). */
|
|
92
|
+
export async function readClaudeMdAuditStamp(devflowDir, maxBytes) {
|
|
93
|
+
const stampPath = join(devflowDir, CLAUDE_MD_AUDIT_STAMP_FILE);
|
|
94
|
+
try {
|
|
95
|
+
const st = await fs.lstat(stampPath);
|
|
96
|
+
if (!st.isFile() || st.size > maxBytes)
|
|
97
|
+
return null;
|
|
98
|
+
return await fs.readFile(stampPath, 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run the audit over the gated roots. Reads only; the stamp is written by
|
|
106
|
+
* {@link writeClaudeMdAuditStamp}. Never throws.
|
|
107
|
+
*/
|
|
108
|
+
export async function runClaudeMdAudit(opts) {
|
|
109
|
+
const loaded = loadClaudeMdAuditModule(opts.scriptsDir);
|
|
110
|
+
if (!loaded.ok)
|
|
111
|
+
return { ok: false, error: { kind: 'load', detail: loaded.error } };
|
|
112
|
+
const audit = loaded.value;
|
|
113
|
+
try {
|
|
114
|
+
const stampText = await readClaudeMdAuditStamp(opts.devflowDir, audit.MAX_STAMP_BYTES);
|
|
115
|
+
const prior = stampText === null ? [] : audit.parseStamp(stampText).keys;
|
|
116
|
+
const roots = claudeMdAuditRoots(opts.claudeDir, opts.projectRoot);
|
|
117
|
+
if (opts.showAll !== true)
|
|
118
|
+
return { ok: true, value: audit.audit({ roots, home: opts.home, keys: prior }) };
|
|
119
|
+
const result = audit.audit({ roots, home: opts.home, keys: [] });
|
|
120
|
+
const keys = [...prior.filter(key => !result.shown.includes(key)), ...result.shown].slice(-audit.MAX_KEYS);
|
|
121
|
+
return { ok: true, value: { ...result, stamp: audit.renderStamp({ roots, examined: result.examined, keys }) } };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return { ok: false, error: { kind: 'failed', detail: err instanceof Error ? err.message : String(err) } };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Run the audit and record its stamp (D-AUDIT-STAMP): what `devflow init` does after the
|
|
129
|
+
* install. Every finding over a threshold is displayed (`showAll`), as init is a deliberate
|
|
130
|
+
* command. The stamp carries the examined paths and every key the audit displayed, so the
|
|
131
|
+
* first SessionStart afterwards repeats nothing it was just shown. A stamp that cannot be
|
|
132
|
+
* written (no machine root, a link in the way, a full disk) costs a repeated message at
|
|
133
|
+
* most, never the audit's result, so that failure is not reported. Never throws.
|
|
134
|
+
*/
|
|
135
|
+
export async function auditAndRecordClaudeMd(opts) {
|
|
136
|
+
const run = await runClaudeMdAudit({ ...opts, showAll: true });
|
|
137
|
+
if (run.ok)
|
|
138
|
+
await writeClaudeMdAuditStamp(opts.devflowDir, run.value.stamp);
|
|
139
|
+
return run;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The display lines of an audit result, as ONE note body, or null when nothing is
|
|
143
|
+
* flagged. The single formatter both init paths (Recommended and Advanced) use; the
|
|
144
|
+
* lines themselves are produced by the script's own formatter, the one the hook
|
|
145
|
+
* shows in its systemMessage.
|
|
146
|
+
*/
|
|
147
|
+
export function formatClaudeMdAuditNote(result) {
|
|
148
|
+
return result.lines.length === 0 ? null : result.lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
/** The one degraded line printed when the audit could not run. */
|
|
151
|
+
export function formatClaudeMdAuditUnavailable(error) {
|
|
152
|
+
if (error.kind === 'load') {
|
|
153
|
+
return error.detail.kind === 'not-found'
|
|
154
|
+
? 'CLAUDE.md import audit unavailable: audit script not found — reinstall devflow-kit'
|
|
155
|
+
: 'CLAUDE.md import audit unavailable: audit script failed to load — reinstall devflow-kit';
|
|
156
|
+
}
|
|
157
|
+
return 'CLAUDE.md import audit unavailable: the audit failed';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Write the stamp text to `<devflowDir>/.claude-md-audit` (AC-437, D-AUDIT-STAMP).
|
|
161
|
+
*
|
|
162
|
+
* Refuses, writing nothing: a machine root that does not exist; a stamp path that is
|
|
163
|
+
* a symbolic link, a directory, a FIFO or any other non-regular file (an `lstat`
|
|
164
|
+
* check made here, because the atomic writer renames over the target without
|
|
165
|
+
* looking at what it is). Otherwise the text lands in a sibling temp file renamed
|
|
166
|
+
* into place. Never throws.
|
|
167
|
+
*/
|
|
168
|
+
export async function writeClaudeMdAuditStamp(devflowDir, text) {
|
|
169
|
+
try {
|
|
170
|
+
const root = await fs.stat(devflowDir).catch(() => null);
|
|
171
|
+
if (root === null || !root.isDirectory())
|
|
172
|
+
return { ok: false, error: { kind: 'no-machine-root' } };
|
|
173
|
+
const stampPath = join(devflowDir, CLAUDE_MD_AUDIT_STAMP_FILE);
|
|
174
|
+
const existing = await fs.lstat(stampPath).catch((err) => {
|
|
175
|
+
if (err.code === 'ENOENT')
|
|
176
|
+
return null;
|
|
177
|
+
throw err;
|
|
178
|
+
});
|
|
179
|
+
if (existing !== null && !existing.isFile()) {
|
|
180
|
+
return { ok: false, error: { kind: 'refused', detail: existing.isSymbolicLink() ? 'the stamp path is a symbolic link' : 'the stamp path is not a regular file' } };
|
|
181
|
+
}
|
|
182
|
+
// Owner-only, as the hook writes it: the stamp names the user's project paths.
|
|
183
|
+
await writeFileAtomicExclusive(stampPath, text, STAMP_CREATE_MODE);
|
|
184
|
+
return { ok: true, value: undefined };
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return { ok: false, error: { kind: 'write-failed', detail: err instanceof Error ? err.message : String(err) } };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=claude-md-audit.js.map
|
|
@@ -172,13 +172,15 @@ function collectMissingFragmentWarnings(fnName, activeFrameworks, fragments, omi
|
|
|
172
172
|
*
|
|
173
173
|
* Every framework reference is installed on every machine
|
|
174
174
|
* (D-COMPLIANCE-INSTALL-ALWAYS), so file presence no longer says which
|
|
175
|
-
* frameworks apply. The
|
|
176
|
-
*
|
|
177
|
-
*
|
|
175
|
+
* frameworks apply. The command names the ids in the agent's prompt
|
|
176
|
+
* (D-COMPLIANCE-INJECT: the compliance directive) — the `COMPLIANCE` field of the
|
|
177
|
+
* settings line, which folds this machine's selection with the repository's
|
|
178
|
+
* `.devflow/project.json` — and those ids alone are in force. No agent takes a
|
|
179
|
+
* named lens input, so the stamp names the prompt, not a variable.
|
|
178
180
|
*/
|
|
179
181
|
const RUNTIME_SELECTION_LINES = [
|
|
180
182
|
'Every `references/{id}.md` is installed, so presence decides nothing: the ids you were given',
|
|
181
|
-
"(
|
|
183
|
+
"(named in your prompt; this machine's plus the repository's) are the frameworks in force.",
|
|
182
184
|
'Load `references/{id}.md` for each given id and no other; `none` means generic controls only.',
|
|
183
185
|
'Apply generic controls always.',
|
|
184
186
|
'',
|
|
@@ -80,6 +80,16 @@ export function setMachineFeature(rawManifest, feature, enabled, now) {
|
|
|
80
80
|
updatedAt: now,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether a RAW parsed manifest already records `feature` as exactly `enabled`:
|
|
85
|
+
* `features.<feature>` is that boolean. Pure. Stricter than
|
|
86
|
+
* {@link isMachineFeatureOn} on purpose — an absent key, a non-boolean value and
|
|
87
|
+
* a legacy key all read as a state without being recorded as it, so writing the
|
|
88
|
+
* explicit boolean is still a change.
|
|
89
|
+
*/
|
|
90
|
+
function isMachineFeatureRecorded(rawManifest, feature, enabled) {
|
|
91
|
+
return isJsonObject(rawManifest) && isJsonObject(rawManifest.features) && rawManifest.features[feature] === enabled;
|
|
92
|
+
}
|
|
83
93
|
async function readRawManifest(devflowDir) {
|
|
84
94
|
try {
|
|
85
95
|
return JSON.parse(await fs.readFile(path.join(devflowDir, 'manifest.json'), 'utf-8'));
|
|
@@ -96,14 +106,44 @@ async function readRawManifest(devflowDir) {
|
|
|
96
106
|
export async function readMachineFeature(devflowDir, feature) {
|
|
97
107
|
return isMachineFeatureOn(await readRawManifest(devflowDir), feature);
|
|
98
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a RAW parsed manifest has an opt-in `feature` switched on. Pure.
|
|
111
|
+
*
|
|
112
|
+
* D-BEHAVIOR-OPT-IN: only an explicit boolean `true` is on. A missing key, a
|
|
113
|
+
* non-boolean value, a manifest without a `features` record and anything that is not an
|
|
114
|
+
* object all read as OFF — fail-closed, the mirror of {@link isMachineFeatureOn}. Like
|
|
115
|
+
* that reader it is deliberately not built on readManifest(): a read-only status must
|
|
116
|
+
* not heal-write, and a manifest missing a required field is still a manifest here.
|
|
117
|
+
*/
|
|
118
|
+
export function isOptInFeatureOn(rawManifest, feature) {
|
|
119
|
+
if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
|
|
120
|
+
return false;
|
|
121
|
+
return rawManifest.features[feature] === true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read an opt-in machine switch from `<devflowDir>/manifest.json`. Read-only (no heal
|
|
125
|
+
* write); an absent, unreadable or malformed manifest reads as OFF.
|
|
126
|
+
*/
|
|
127
|
+
export async function readOptInFeature(devflowDir, feature) {
|
|
128
|
+
return isOptInFeatureOn(await readRawManifest(devflowDir), feature);
|
|
129
|
+
}
|
|
99
130
|
/**
|
|
100
131
|
* Write the machine-wide switch to `<devflowDir>/manifest.json` (atomic
|
|
101
132
|
* temp + rename). Refuses with `not-installed` when there is no manifest to
|
|
102
133
|
* write into — a manifest is created by `devflow init`, never by a toggle,
|
|
103
134
|
* because a bare `{features: {...}}` file is not a manifest any reader accepts.
|
|
135
|
+
*
|
|
136
|
+
* D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
|
|
137
|
+
* nothing. The only bytes such a write could change are `updatedAt`, and that
|
|
138
|
+
* field means "the manifest's content last changed", so a no-op must not move
|
|
139
|
+
* it. This is the one write point of `devflow memory|learning|knowledge|behavior
|
|
140
|
+
* --enable/--disable`, so the rule holds for all four at once.
|
|
104
141
|
*/
|
|
105
142
|
export async function writeMachineFeature(devflowDir, feature, enabled) {
|
|
106
|
-
const
|
|
143
|
+
const raw = await readRawManifest(devflowDir);
|
|
144
|
+
if (isMachineFeatureRecorded(raw, feature, enabled))
|
|
145
|
+
return { ok: true, value: undefined };
|
|
146
|
+
const next = setMachineFeature(raw, feature, enabled, new Date().toISOString());
|
|
107
147
|
if (next === null)
|
|
108
148
|
return { ok: false, error: 'not-installed' };
|
|
109
149
|
await writeFileAtomicExclusive(path.join(devflowDir, 'manifest.json'), JSON.stringify(next, null, 2) + '\n');
|
package/dist/core/flags.js
CHANGED
|
@@ -88,6 +88,45 @@ export const FLAG_REGISTRY = [
|
|
|
88
88
|
recommended: true,
|
|
89
89
|
defaultValue: true,
|
|
90
90
|
},
|
|
91
|
+
{
|
|
92
|
+
// D-SUBAGENT-CACHE-5M: subagents write their prompt cache with the 5-minute TTL
|
|
93
|
+
// while the main session keeps the 1-hour TTL of `prompt-caching-1h` (untouched).
|
|
94
|
+
//
|
|
95
|
+
// Why: the wave-4 token audit (2026-10-10) found 99.998% of subagent cache writes
|
|
96
|
+
// are 1-hour writes, yet fewer than 0.1% of subagent requests idle longer than
|
|
97
|
+
// 5 minutes - a subagent finishes or is resumed long before a 5-minute entry
|
|
98
|
+
// lapses, so the 1-hour premium buys nothing. Modelled effect of 5m: -8.9% on
|
|
99
|
+
// subagent cache-equivalent cost, -7.7% on total.
|
|
100
|
+
//
|
|
101
|
+
// Mechanism (VENDOR BEHAVIOUR, probed 2026-10-11 on Claude Code 2.1.296 in a
|
|
102
|
+
// sandboxed session): with ENABLE_PROMPT_CACHING_1H=true and this setting at
|
|
103
|
+
// "5m", subagent requests wrote only ephemeral_5m cache (36,387 tokens, none at
|
|
104
|
+
// 1h) while the main session wrote 100% 1h (35,209). Precedence of the subagent
|
|
105
|
+
// TTL in the binary, highest first: an env var that forces 5m, env
|
|
106
|
+
// CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, this setting, a per-agent frontmatter
|
|
107
|
+
// `experimental.cacheTtl`, ENABLE_PROMPT_CACHING_1H. The 1-hour TTL is ignored
|
|
108
|
+
// while a subscription is in overage. A Claude Code that predates the setting
|
|
109
|
+
// ignores the key, so subagents then follow ENABLE_PROMPT_CACHING_1H as before.
|
|
110
|
+
// Re-probe when the precedence matters again; this is an observation, not a spec.
|
|
111
|
+
//
|
|
112
|
+
// Rejected: per-agent `experimental.cacheTtl` - it would spread one policy over
|
|
113
|
+
// every agent file; one devflow-managed flag keeps the policy in one place.
|
|
114
|
+
//
|
|
115
|
+
// Delete guard: only "5m" is deleted on disable or uninstall, so a user's own
|
|
116
|
+
// "1h" stays (D-GUARD-SCALAR). Adoption: init seeds this flag OFF when a value
|
|
117
|
+
// that is not "5m" is already on disk (D-ADOPT-RESPECTS-USER, respectUserOwnedSettings).
|
|
118
|
+
id: 'subagent-cache-5m',
|
|
119
|
+
label: 'Subagent 5-min cache',
|
|
120
|
+
description: 'Write subagent prompt-cache entries with the 5-minute TTL; the main session keeps 1 hour',
|
|
121
|
+
hint: 'Subagents rarely idle past 5 min, so 5-min cache writes cost less',
|
|
122
|
+
blurb: '5-min cache TTL for subagents',
|
|
123
|
+
kind: 'boolean',
|
|
124
|
+
target: { type: 'setting', key: 'subagentPromptCacheTtl' },
|
|
125
|
+
onPayload: '5m',
|
|
126
|
+
settingDeleteGuard: '5m',
|
|
127
|
+
recommended: true,
|
|
128
|
+
defaultValue: true,
|
|
129
|
+
},
|
|
91
130
|
{
|
|
92
131
|
id: 'show-turn-duration',
|
|
93
132
|
label: 'Show turn duration',
|
|
@@ -423,6 +462,34 @@ export const FLAG_REGISTRY = [
|
|
|
423
462
|
integer: true,
|
|
424
463
|
upstreamDefault: 600000,
|
|
425
464
|
},
|
|
465
|
+
{
|
|
466
|
+
// D-AUTO-COMPACT-WINDOW-OPT-IN: opt-in and unset by default (undefined → manifest null →
|
|
467
|
+
// key never written). devflow writes CLAUDE_CODE_AUTO_COMPACT_WINDOW only when the user sets
|
|
468
|
+
// this flag. Init applies a seeded record without asking, which is why the default is neutral
|
|
469
|
+
// and `recommended` stays false: a smaller window compacts sooner, and a compaction
|
|
470
|
+
// mid-command is exactly what the SessionStart resume directive (D-COMPACT-RESUME-DIRECTIVE)
|
|
471
|
+
// recovers from, so the flag is not recommended until a forced mid-/implement /compact has
|
|
472
|
+
// been seen to resume at the phase after the last finished one (plan §9, a follow-up gate).
|
|
473
|
+
// The percent-based auto-compact override is a different variable that devflow deliberately
|
|
474
|
+
// never writes or names; tests/guards/no-autocompact-pct-override.test.ts keeps it out of src/.
|
|
475
|
+
// The env name was confirmed present in Claude Code 2.1.296, where it takes precedence over
|
|
476
|
+
// the in-app setting, and an in-app message names `auto` or 100k–1M (docs/reference/
|
|
477
|
+
// claude-code-flags-probe.md). No upstreamDefault: the default window and the grammar of the
|
|
478
|
+
// variable's own value were not observed, so the description states only devflow's own
|
|
479
|
+
// accepted range, 100000–1000000.
|
|
480
|
+
id: 'auto-compact-window',
|
|
481
|
+
label: 'Auto-compact window',
|
|
482
|
+
description: 'Context window size in tokens at which Claude Code auto-compacts',
|
|
483
|
+
hint: 'Sets the auto-compact window (100000-1000000); unset keeps the default',
|
|
484
|
+
blurb: 'auto-compact window size',
|
|
485
|
+
kind: 'number',
|
|
486
|
+
target: { type: 'env', key: 'CLAUDE_CODE_AUTO_COMPACT_WINDOW' },
|
|
487
|
+
recommended: false,
|
|
488
|
+
defaultValue: undefined,
|
|
489
|
+
min: 100000, // devflow sanity bound
|
|
490
|
+
max: 1000000, // devflow sanity bound
|
|
491
|
+
integer: true,
|
|
492
|
+
},
|
|
426
493
|
{
|
|
427
494
|
// Writes as { command: value } per Claude Code spellcheck setting shape.
|
|
428
495
|
id: 'spellcheck',
|
|
@@ -941,6 +1008,78 @@ export function settingHoldsManagedShape(settingsJson, flagId) {
|
|
|
941
1008
|
return false;
|
|
942
1009
|
}
|
|
943
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* D-ADOPT-RESPECTS-USER predicate: the mirror of `settingHoldsManagedShape`.
|
|
1013
|
+
*
|
|
1014
|
+
* Returns true when the settings.json string carries a value at the guarded flag's
|
|
1015
|
+
* target key that is NOT the flag's managed shape - a value the user (or another
|
|
1016
|
+
* tool) owns, which enabling the flag would overwrite. Presence is decided with
|
|
1017
|
+
* `hasOwnProperty`, so an explicit `null` counts as a value; the shape decision is
|
|
1018
|
+
* delegated to `settingValueHoldsManagedShape`, never compared here.
|
|
1019
|
+
*
|
|
1020
|
+
* Returns false when:
|
|
1021
|
+
* - settingsJson is malformed
|
|
1022
|
+
* - root is not a plain object
|
|
1023
|
+
* - flagId is not in the registry
|
|
1024
|
+
* - the flag has no settingDeleteGuard, or is not a setting-target boolean
|
|
1025
|
+
* - the key is absent
|
|
1026
|
+
* - the on-disk value holds the managed shape
|
|
1027
|
+
*/
|
|
1028
|
+
export function settingHoldsUserValue(settingsJson, flagId) {
|
|
1029
|
+
try {
|
|
1030
|
+
const parsed = JSON.parse(settingsJson);
|
|
1031
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
1032
|
+
return false;
|
|
1033
|
+
const flag = FLAG_REGISTRY_MAP.get(flagId);
|
|
1034
|
+
if (!flag || flag.kind !== 'boolean' || flag.target.type !== 'setting')
|
|
1035
|
+
return false;
|
|
1036
|
+
if (flag.settingDeleteGuard === undefined)
|
|
1037
|
+
return false;
|
|
1038
|
+
const root = parsed;
|
|
1039
|
+
if (!Object.prototype.hasOwnProperty.call(root, flag.target.key))
|
|
1040
|
+
return false;
|
|
1041
|
+
return !settingValueHoldsManagedShape(flag, root[flag.target.key]);
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* D-ADOPT-RESPECTS-USER: seed a guarded setting flag OFF when adopting it ON would
|
|
1049
|
+
* overwrite a value the user already owns.
|
|
1050
|
+
*
|
|
1051
|
+
* A flag the manifest does not record is adopted at its `defaultValue`. For a flag whose
|
|
1052
|
+
* target is a settings.json key with a `settingDeleteGuard`, adopting ON writes its
|
|
1053
|
+
* managed payload over whatever is on disk (the guard protects deletion, not writes).
|
|
1054
|
+
* When the key already holds a value that is not the managed shape - a user's own `"1h"`
|
|
1055
|
+
* under `subagentPromptCacheTtl` - that write would silently replace a deliberate choice.
|
|
1056
|
+
* Such a flag is set `false` instead; with the record then claiming `false`, the converge
|
|
1057
|
+
* pipeline neither writes nor deletes the user's value.
|
|
1058
|
+
*
|
|
1059
|
+
* Scope: only flags ABSENT from `manifestFlags` (or all of them on a fresh install, when
|
|
1060
|
+
* it is null). A flag the manifest records keeps its value in `flags`, so an explicit
|
|
1061
|
+
* enable still wins. `devflow init --reset` hands this an empty settings snapshot
|
|
1062
|
+
* (resolveResetGatedInputs), so nothing is detected and the factory default stands. The rule is generic over guarded
|
|
1063
|
+
* setting flags: no flag id is special-cased, and `settingHoldsUserValue` is the one place
|
|
1064
|
+
* that decides "the disk holds something that is not ours". For a flag whose default is
|
|
1065
|
+
* already OFF (suppress-attribution) it changes nothing.
|
|
1066
|
+
*
|
|
1067
|
+
* Two callers apply the same rule: the init seed, and the flags TUI's starting rows (an
|
|
1068
|
+
* upgrade that has not run init yet would otherwise offer a Save that overwrites the value).
|
|
1069
|
+
*
|
|
1070
|
+
* Pure function - the settings snapshot is read by the caller and passed in.
|
|
1071
|
+
*/
|
|
1072
|
+
export function respectUserOwnedSettings(flags, manifestFlags, settingsSnapshot) {
|
|
1073
|
+
const result = { ...flags };
|
|
1074
|
+
for (const flag of FLAG_REGISTRY) {
|
|
1075
|
+
const adoptedAtDefault = manifestFlags === null || !(flag.id in manifestFlags);
|
|
1076
|
+
if (!adoptedAtDefault)
|
|
1077
|
+
continue;
|
|
1078
|
+
if (settingHoldsUserValue(settingsSnapshot, flag.id))
|
|
1079
|
+
result[flag.id] = false;
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
944
1083
|
/**
|
|
945
1084
|
* D-ATTR-GUARD: returns true when it is safe to delete a flag's target key from
|
|
946
1085
|
* settings. For flags with a settingDeleteGuard the key is only deleted when the
|
package/dist/core/fs-atomic.js
CHANGED
|
@@ -28,15 +28,20 @@ import { promises as fs } from 'fs';
|
|
|
28
28
|
*
|
|
29
29
|
* @param filePath - Absolute path to the target file.
|
|
30
30
|
* @param data - UTF-8 encoded content to write.
|
|
31
|
+
* @param createMode - Permission bits the temp file is CREATED with (masked by the
|
|
32
|
+
* umask), so a file that must be owner-only is never readable by others, not even
|
|
33
|
+
* between the write and the rename. Omitted, the umask default applies. Either way
|
|
34
|
+
* an existing target's mode wins (below).
|
|
31
35
|
*/
|
|
32
|
-
export async function writeFileAtomicExclusive(filePath, data) {
|
|
36
|
+
export async function writeFileAtomicExclusive(filePath, data, createMode) {
|
|
33
37
|
// PID-scope the tmp name so concurrent writers from different processes
|
|
34
38
|
// (e.g., two Claude Code sessions) never collide on the same .tmp path.
|
|
35
39
|
// mirrors proxy-log.ts rotation at src/core/proxy-log.ts which PID-scopes
|
|
36
40
|
// for the same reason.
|
|
37
41
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
42
|
+
const options = { encoding: 'utf-8', flag: 'wx', ...(createMode === undefined ? {} : { mode: createMode }) };
|
|
38
43
|
try {
|
|
39
|
-
await fs.writeFile(tmp, data,
|
|
44
|
+
await fs.writeFile(tmp, data, options);
|
|
40
45
|
}
|
|
41
46
|
catch (err) {
|
|
42
47
|
if (err.code !== 'EEXIST')
|
|
@@ -48,7 +53,7 @@ export async function writeFileAtomicExclusive(filePath, data) {
|
|
|
48
53
|
await fs.unlink(tmp);
|
|
49
54
|
}
|
|
50
55
|
catch { /* race — already removed */ }
|
|
51
|
-
await fs.writeFile(tmp, data,
|
|
56
|
+
await fs.writeFile(tmp, data, options);
|
|
52
57
|
}
|
|
53
58
|
// Preserve the target's permission mode across the atomic replace.
|
|
54
59
|
// A user who hardened the target (e.g. settings.json → 0600 to protect
|