devflow-kit 3.3.0 → 3.4.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 +18 -0
- package/dist/agents/code.md +330 -0
- package/{src/assets → dist}/agents/design.md +1 -1
- package/{src/assets → dist}/agents/diagnose.md +1 -2
- package/dist/agents/git.md +29 -56
- package/{src/assets → dist}/agents/knowledge.md +4 -3
- package/{src/assets → dist}/agents/research.md +2 -2
- package/{src/assets → dist}/agents/review.md +8 -7
- package/{src/assets → dist}/agents/scrutinize.md +1 -1
- package/dist/agents/skim.md +148 -0
- package/{src/assets → dist}/agents/triage.md +1 -1
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/learning.js +38 -3
- package/dist/cli/commands/uninstall.js +42 -1
- package/dist/commands/bug-analysis.md +30 -8
- package/dist/commands/code-review.md +141 -60
- package/dist/commands/debug.md +14 -12
- package/dist/commands/dynamic-build.md +37 -38
- package/dist/commands/dynamic-plan.md +30 -18
- package/dist/commands/dynamic-profile.md +27 -13
- package/dist/commands/dynamic-tickets.md +28 -14
- package/dist/commands/explore.md +15 -13
- package/dist/commands/implement.md +33 -28
- package/dist/commands/plan.md +37 -24
- package/dist/commands/release.md +69 -4
- package/dist/commands/research.md +33 -11
- package/dist/commands/resolve.md +35 -32
- package/dist/commands/self-review.md +36 -23
- 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/feature-switch.js +20 -1
- package/dist/core/flags.js +28 -0
- package/dist/core/fs-atomic.js +8 -3
- package/dist/core/learning-variants.js +213 -0
- package/dist/core/manifest.js +62 -0
- package/dist/core/mds-variants.js +38 -1
- package/dist/core/plugins.js +71 -9
- package/{src/assets → dist/learning-off}/agents/code.md +6 -10
- package/dist/learning-off/agents/design.md +119 -0
- package/dist/learning-off/agents/diagnose.md +210 -0
- package/dist/learning-off/agents/knowledge.md +90 -0
- package/dist/learning-off/agents/research.md +149 -0
- package/dist/learning-off/agents/review.md +228 -0
- package/dist/learning-off/agents/scrutinize.md +117 -0
- package/{src/assets → dist/learning-off}/agents/skim.md +1 -8
- package/dist/learning-off/agents/triage.md +163 -0
- package/dist/learning-off/commands/bug-analysis.md +420 -0
- package/dist/learning-off/commands/code-review.md +525 -0
- package/dist/learning-off/commands/debug.md +294 -0
- package/dist/learning-off/commands/dynamic-build.md +1255 -0
- package/dist/learning-off/commands/dynamic-plan.md +424 -0
- package/dist/learning-off/commands/dynamic-profile.md +214 -0
- package/dist/learning-off/commands/dynamic-tickets.md +632 -0
- package/dist/learning-off/commands/explore.md +210 -0
- package/dist/learning-off/commands/implement.md +808 -0
- package/dist/learning-off/commands/plan.md +664 -0
- package/dist/learning-off/commands/release.md +310 -0
- package/dist/learning-off/commands/research.md +222 -0
- package/dist/learning-off/commands/resolve.md +837 -0
- package/dist/learning-off/commands/self-review.md +266 -0
- package/dist/skills/git/references/tracker/_contract.md +33 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +4 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +12 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +1 -1
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +4 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +14 -2
- package/dist/skills/git/references/tracker/linear/associate-release.md +1 -1
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +4 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +14 -2
- package/dist/targets/claude-code/installer.js +72 -36
- package/dist/targets/claude-code/language-stamp.js +185 -0
- package/dist/targets/claude-code/learning-install.js +489 -0
- package/package.json +1 -1
- package/src/assets/agents/code.mds +339 -0
- package/src/assets/agents/design.mds +149 -0
- package/src/assets/agents/diagnose.mds +225 -0
- package/src/assets/agents/evaluate.md +1 -3
- package/src/assets/agents/git.mds +29 -56
- package/src/assets/agents/knowledge.mds +125 -0
- package/src/assets/agents/research.mds +176 -0
- package/src/assets/agents/review.mds +286 -0
- package/src/assets/agents/scrutinize.mds +132 -0
- package/src/assets/agents/skim.mds +161 -0
- package/src/assets/agents/triage.mds +194 -0
- package/src/assets/agents/validate.md +8 -6
- package/src/assets/commands/_partials/_compliance.mds +5 -4
- package/src/assets/commands/_partials/_decisions.mds +31 -0
- package/src/assets/commands/_partials/_engine.mds +9 -1
- package/src/assets/commands/_partials/_knowledge.mds +25 -12
- package/src/assets/commands/_partials/_preamble.mds +33 -9
- package/src/assets/commands/_partials/_publication.mds +5 -4
- package/src/assets/commands/_partials/_settings.mds +13 -5
- package/src/assets/commands/_partials/_wave.mds +8 -0
- package/src/assets/commands/bug-analysis.mds +24 -2
- package/src/assets/commands/code-review.mds +147 -44
- package/src/assets/commands/debug.mds +17 -1
- package/src/assets/commands/dynamic-build.mds +33 -2
- package/src/assets/commands/dynamic-plan.mds +36 -6
- package/src/assets/commands/dynamic-profile.mds +9 -1
- package/src/assets/commands/dynamic-tickets.mds +16 -2
- package/src/assets/commands/explore.mds +27 -1
- package/src/assets/commands/implement.mds +41 -8
- package/src/assets/commands/plan.mds +47 -8
- package/src/assets/commands/{release.md → release.mds} +27 -24
- package/src/assets/commands/research.mds +28 -4
- package/src/assets/commands/resolve.mds +43 -2
- package/src/assets/commands/self-review.mds +30 -5
- package/src/assets/mds/tracker/_contract.mds +72 -0
- package/src/assets/mds/tracker/_github.mds +13 -2
- package/src/assets/mds/tracker/_jira.mds +17 -5
- package/src/assets/mds/tracker/_linear.mds +17 -5
- package/src/assets/mds/tracker/_mcp.mds +2 -2
- package/src/assets/mds/tracker/_steps.mds +97 -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 +1 -2
- package/src/assets/scripts/hooks/json-helper.cjs +13 -5
- package/src/assets/scripts/hooks/json-parse +34 -10
- package/src/assets/scripts/hooks/session-start-context +315 -7
- 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/feature-knowledge/SKILL.md +43 -12
- package/src/assets/skills/quality-gates/SKILL.md +1 -1
|
@@ -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
|
|
@@ -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'));
|
|
@@ -101,9 +111,18 @@ export async function readMachineFeature(devflowDir, feature) {
|
|
|
101
111
|
* temp + rename). Refuses with `not-installed` when there is no manifest to
|
|
102
112
|
* write into — a manifest is created by `devflow init`, never by a toggle,
|
|
103
113
|
* because a bare `{features: {...}}` file is not a manifest any reader accepts.
|
|
114
|
+
*
|
|
115
|
+
* D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
|
|
116
|
+
* nothing. The only bytes such a write could change are `updatedAt`, and that
|
|
117
|
+
* field means "the manifest's content last changed", so a no-op must not move
|
|
118
|
+
* it. This is the one write point of `devflow memory|learning|knowledge
|
|
119
|
+
* --enable/--disable`, so the rule holds for all three at once.
|
|
104
120
|
*/
|
|
105
121
|
export async function writeMachineFeature(devflowDir, feature, enabled) {
|
|
106
|
-
const
|
|
122
|
+
const raw = await readRawManifest(devflowDir);
|
|
123
|
+
if (isMachineFeatureRecorded(raw, feature, enabled))
|
|
124
|
+
return { ok: true, value: undefined };
|
|
125
|
+
const next = setMachineFeature(raw, feature, enabled, new Date().toISOString());
|
|
107
126
|
if (next === null)
|
|
108
127
|
return { ok: false, error: 'not-installed' };
|
|
109
128
|
await writeFileAtomicExclusive(path.join(devflowDir, 'manifest.json'), JSON.stringify(next, null, 2) + '\n');
|
package/dist/core/flags.js
CHANGED
|
@@ -423,6 +423,34 @@ export const FLAG_REGISTRY = [
|
|
|
423
423
|
integer: true,
|
|
424
424
|
upstreamDefault: 600000,
|
|
425
425
|
},
|
|
426
|
+
{
|
|
427
|
+
// D-AUTO-COMPACT-WINDOW-OPT-IN: opt-in and unset by default (undefined → manifest null →
|
|
428
|
+
// key never written). devflow writes CLAUDE_CODE_AUTO_COMPACT_WINDOW only when the user sets
|
|
429
|
+
// this flag. Init applies a seeded record without asking, which is why the default is neutral
|
|
430
|
+
// and `recommended` stays false: a smaller window compacts sooner, and a compaction
|
|
431
|
+
// mid-command is exactly what the SessionStart resume directive (D-COMPACT-RESUME-DIRECTIVE)
|
|
432
|
+
// recovers from, so the flag is not recommended until a forced mid-/implement /compact has
|
|
433
|
+
// been seen to resume at the phase after the last finished one (plan §9, a follow-up gate).
|
|
434
|
+
// The percent-based auto-compact override is a different variable that devflow deliberately
|
|
435
|
+
// never writes or names; tests/guards/no-autocompact-pct-override.test.ts keeps it out of src/.
|
|
436
|
+
// The env name was confirmed present in Claude Code 2.1.296, where it takes precedence over
|
|
437
|
+
// the in-app setting, and an in-app message names `auto` or 100k–1M (docs/reference/
|
|
438
|
+
// claude-code-flags-probe.md). No upstreamDefault: the default window and the grammar of the
|
|
439
|
+
// variable's own value were not observed, so the description states only devflow's own
|
|
440
|
+
// accepted range, 100000–1000000.
|
|
441
|
+
id: 'auto-compact-window',
|
|
442
|
+
label: 'Auto-compact window',
|
|
443
|
+
description: 'Context window size in tokens at which Claude Code auto-compacts',
|
|
444
|
+
hint: 'Sets the auto-compact window (100000-1000000); unset keeps the default',
|
|
445
|
+
blurb: 'auto-compact window size',
|
|
446
|
+
kind: 'number',
|
|
447
|
+
target: { type: 'env', key: 'CLAUDE_CODE_AUTO_COMPACT_WINDOW' },
|
|
448
|
+
recommended: false,
|
|
449
|
+
defaultValue: undefined,
|
|
450
|
+
min: 100000, // devflow sanity bound
|
|
451
|
+
max: 1000000, // devflow sanity bound
|
|
452
|
+
integer: true,
|
|
453
|
+
},
|
|
426
454
|
{
|
|
427
455
|
// Writes as { command: value } per Claude Code spellcheck setting shape.
|
|
428
456
|
id: 'spellcheck',
|
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
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning-variant splitter: one compiled prompt body in, its learning-on and
|
|
3
|
+
* learning-off variants out.
|
|
4
|
+
*
|
|
5
|
+
* Pure module, zero I/O. Every refusal is a Result; the exiting shell is
|
|
6
|
+
* scripts/build-mds.ts, which renders these errors into the build's single exit.
|
|
7
|
+
*
|
|
8
|
+
* D-LEARNING-VARIANTS: when learning is off on a machine, the decisions text is
|
|
9
|
+
* ABSENT from every prompt it would have reached, not merely gated at run time.
|
|
10
|
+
* The build therefore emits two variants of a prompt that carries learning text:
|
|
11
|
+
* the learning-on variant at today's path (dist/commands/x.md, dist/agents/x.md)
|
|
12
|
+
* and the learning-off variant at dist/learning-off/{commands,agents}/x.md, only
|
|
13
|
+
* for a host that has an arm. A prompt with no arm has one variant and no
|
|
14
|
+
* learning-off file.
|
|
15
|
+
*
|
|
16
|
+
* The arms are whole-line markers in the host or in a partial it expands:
|
|
17
|
+
*
|
|
18
|
+
* <!-- learning:on --> opens an arm that only the learning-on variant keeps
|
|
19
|
+
* <!-- learning:off --> opens an arm that only the learning-off variant keeps;
|
|
20
|
+
* directly after an on arm it is that arm's else
|
|
21
|
+
* <!-- learning:end --> closes the arm
|
|
22
|
+
*
|
|
23
|
+
* A marker line matches LEARNING_MARKER_RE. It may be indented and may sit inside
|
|
24
|
+
* a fenced block, a list, a define body or an agent's second frontmatter block,
|
|
25
|
+
* because MDS passes an HTML comment through untouched. The splitter runs on the
|
|
26
|
+
* COMPILED body, after the MDS compile, so it adds no compileFile call, no import
|
|
27
|
+
* into an MDS module and nothing against the per-module compile-time guard. A
|
|
28
|
+
* marker line is removed whole, with its newline. Every other line is kept byte
|
|
29
|
+
* for byte, so a blank line is content: put the blank line a paragraph needs
|
|
30
|
+
* inside the arm that owns the paragraph, and the other variant gets none of it.
|
|
31
|
+
*
|
|
32
|
+
* The markers are INTERIM. MDS 0.4.4 emits an `@if` inside a code fence or on an
|
|
33
|
+
* indented line as literal text (its lexer requires column 0), and the arms have
|
|
34
|
+
* to sit exactly there: inside fenced spawn blocks, scripts, list items and
|
|
35
|
+
* frontmatter. They are replaced by native conditionals once mdscript ships
|
|
36
|
+
* fence- and indent-aware `@if` (dean0x/mdscript#452). That swap, with the
|
|
37
|
+
* @mdscript/mds upgrade it rides on proven byte-identical in dist/ and this module
|
|
38
|
+
* deleted, is dean0x/devflow#439. The mapping is one to one: `on` becomes
|
|
39
|
+
* `@if learning:`, `off` becomes `@else:` and `end` becomes `@end`. A standalone
|
|
40
|
+
* `off` arm, with no on arm before it, becomes an `@if learning:` with an empty
|
|
41
|
+
* body followed by `@else:`.
|
|
42
|
+
*
|
|
43
|
+
* What the splitter refuses, each as a LearningSplitError with the source line:
|
|
44
|
+
* - nesting: an `on` marker while an arm is open, or a second `off` in one arm;
|
|
45
|
+
* - an `end` with no open arm;
|
|
46
|
+
* - an arm still open at the end of the body;
|
|
47
|
+
* - an empty arm (no line with content between its marker and the next);
|
|
48
|
+
* - a stray marker: a line that names `<!-- learning:` but is not a whole-line
|
|
49
|
+
* marker, which would otherwise ship as prompt text.
|
|
50
|
+
*
|
|
51
|
+
* Scope guarantee: this module answers one question (what are the two variants of
|
|
52
|
+
* this body?) and one cheap one for the build to ask of a body that must carry no
|
|
53
|
+
* arms (containsLearningMarker). It decides nothing about WHICH text belongs in an
|
|
54
|
+
* arm: that is authoring, and the guards over the built trees hold it.
|
|
55
|
+
*/
|
|
56
|
+
function Ok(value) {
|
|
57
|
+
return { ok: true, value };
|
|
58
|
+
}
|
|
59
|
+
function Err(error) {
|
|
60
|
+
return { ok: false, error };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Repo-relative root of the learning-off variants.
|
|
64
|
+
*
|
|
65
|
+
* Exported because the build's orphan prune names this directory even when no
|
|
66
|
+
* host carries an arm, which is exactly the case where every file in it is an
|
|
67
|
+
* orphan, so it cannot be derived from the plan.
|
|
68
|
+
*/
|
|
69
|
+
export const LEARNING_OFF_OUTPUT_DIR = 'dist/learning-off';
|
|
70
|
+
/**
|
|
71
|
+
* POSIX repo-relative path of the learning-off variant of an output file.
|
|
72
|
+
*
|
|
73
|
+
* `fileName` is the already-validated basename the learning-on variant is written
|
|
74
|
+
* under (`implement.md`), so the two variants of one host always share a name.
|
|
75
|
+
*/
|
|
76
|
+
export function learningOffRelPath(kind, fileName) {
|
|
77
|
+
return `${LEARNING_OFF_OUTPUT_DIR}/${kind}/${fileName}`;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Markers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* A whole-line learning marker. The group captures `on`, `off` or `end`.
|
|
84
|
+
*
|
|
85
|
+
* Anchored at both ends with no nested quantifier, so a match is linear in the
|
|
86
|
+
* line length. Leading and trailing space or tab is allowed; anything else on the
|
|
87
|
+
* line is not a marker.
|
|
88
|
+
*/
|
|
89
|
+
export const LEARNING_MARKER_RE = /^[ \t]*<!-- learning:(on|off|end) -->[ \t]*$/;
|
|
90
|
+
/**
|
|
91
|
+
* The fragment that makes a line a marker ATTEMPT: loose on spacing and case so a
|
|
92
|
+
* malformed marker is caught rather than shipped. Every valid marker contains it.
|
|
93
|
+
*/
|
|
94
|
+
const MARKER_FRAGMENT_RE = /<!--\s*learning\s*:/i;
|
|
95
|
+
/** True when the text names a learning marker anywhere, valid or not. */
|
|
96
|
+
export function containsLearningMarker(text) {
|
|
97
|
+
return MARKER_FRAGMENT_RE.test(text);
|
|
98
|
+
}
|
|
99
|
+
function markerOf(line) {
|
|
100
|
+
// A CRLF body carries the carriage return on the line; it is not part of the marker.
|
|
101
|
+
const bare = line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
102
|
+
const match = LEARNING_MARKER_RE.exec(bare);
|
|
103
|
+
return match === null ? null : match[1];
|
|
104
|
+
}
|
|
105
|
+
/** The longest stretch of a stray line a refusal quotes. */
|
|
106
|
+
const STRAY_QUOTE_MAX = 80;
|
|
107
|
+
/**
|
|
108
|
+
* Split a compiled body into its learning-on and learning-off variants.
|
|
109
|
+
*
|
|
110
|
+
* Total and pure: the same input gives the same output, and a refusal is a
|
|
111
|
+
* Result. The loop is one pass over the body's lines, so its bound is the body's
|
|
112
|
+
* own length.
|
|
113
|
+
*
|
|
114
|
+
* An arm needs a line with content in it. An arm that holds only blank lines is
|
|
115
|
+
* refused as empty, because it would add blank lines to one variant and nothing to
|
|
116
|
+
* the other, which is a mistake rather than a design.
|
|
117
|
+
*/
|
|
118
|
+
export function splitLearningVariants(body) {
|
|
119
|
+
const lines = body.split('\n');
|
|
120
|
+
const on = [];
|
|
121
|
+
const off = [];
|
|
122
|
+
let hasArms = false;
|
|
123
|
+
// The open arm, if any: which one, the line of the marker that opened it, and
|
|
124
|
+
// whether it has held a line with content yet.
|
|
125
|
+
let arm = null;
|
|
126
|
+
let armOpenedAt = 0;
|
|
127
|
+
let armHasContent = false;
|
|
128
|
+
for (let index = 0; index < lines.length; index++) {
|
|
129
|
+
const line = lines[index];
|
|
130
|
+
const lineNo = index + 1;
|
|
131
|
+
const marker = markerOf(line);
|
|
132
|
+
if (marker === null) {
|
|
133
|
+
if (MARKER_FRAGMENT_RE.test(line)) {
|
|
134
|
+
return Err({ kind: 'stray-marker', line: lineNo, text: line.trim().slice(0, STRAY_QUOTE_MAX) });
|
|
135
|
+
}
|
|
136
|
+
if (arm !== 'off')
|
|
137
|
+
on.push(line);
|
|
138
|
+
if (arm !== 'on')
|
|
139
|
+
off.push(line);
|
|
140
|
+
if (arm !== null && line.trim() !== '')
|
|
141
|
+
armHasContent = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
hasArms = true;
|
|
145
|
+
switch (marker) {
|
|
146
|
+
case 'on':
|
|
147
|
+
if (arm !== null)
|
|
148
|
+
return Err({ kind: 'nested-arm', line: lineNo, arm, openedAt: armOpenedAt });
|
|
149
|
+
arm = 'on';
|
|
150
|
+
armOpenedAt = lineNo;
|
|
151
|
+
armHasContent = false;
|
|
152
|
+
break;
|
|
153
|
+
case 'off':
|
|
154
|
+
if (arm === 'off')
|
|
155
|
+
return Err({ kind: 'repeated-off', line: lineNo, openedAt: armOpenedAt });
|
|
156
|
+
// Directly after an on arm this is its else: the on arm ends here.
|
|
157
|
+
if (arm === 'on' && !armHasContent) {
|
|
158
|
+
return Err({ kind: 'empty-arm', arm, openedAt: armOpenedAt, closedAt: lineNo });
|
|
159
|
+
}
|
|
160
|
+
arm = 'off';
|
|
161
|
+
armOpenedAt = lineNo;
|
|
162
|
+
armHasContent = false;
|
|
163
|
+
break;
|
|
164
|
+
case 'end':
|
|
165
|
+
if (arm === null)
|
|
166
|
+
return Err({ kind: 'unopened-end', line: lineNo });
|
|
167
|
+
if (!armHasContent) {
|
|
168
|
+
return Err({ kind: 'empty-arm', arm, openedAt: armOpenedAt, closedAt: lineNo });
|
|
169
|
+
}
|
|
170
|
+
arm = null;
|
|
171
|
+
break;
|
|
172
|
+
default: {
|
|
173
|
+
const unhandled = marker;
|
|
174
|
+
return Err({ kind: 'stray-marker', line: lineNo, text: String(unhandled) });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (arm !== null)
|
|
179
|
+
return Err({ kind: 'unclosed-arm', arm, openedAt: armOpenedAt });
|
|
180
|
+
return Ok({ on: on.join('\n'), off: off.join('\n'), hasArms });
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Render a refusal as one line a person can act on.
|
|
184
|
+
*
|
|
185
|
+
* One arm per kind with a `never` default, so a kind added to the union cannot
|
|
186
|
+
* fall through into a message that does not describe it. Shared by the build and
|
|
187
|
+
* the tests, so the text asserted is the text shipped.
|
|
188
|
+
*/
|
|
189
|
+
export function describeLearningSplitError(error) {
|
|
190
|
+
switch (error.kind) {
|
|
191
|
+
case 'nested-arm':
|
|
192
|
+
return (`line ${error.line}: a learning:on marker inside the ${error.arm} arm opened at line ` +
|
|
193
|
+
`${error.openedAt} — arms do not nest; close it with learning:end first`);
|
|
194
|
+
case 'repeated-off':
|
|
195
|
+
return (`line ${error.line}: a second learning:off marker inside the off arm opened at line ` +
|
|
196
|
+
`${error.openedAt} — an arm has at most one else`);
|
|
197
|
+
case 'unopened-end':
|
|
198
|
+
return `line ${error.line}: a learning:end marker with no open arm`;
|
|
199
|
+
case 'unclosed-arm':
|
|
200
|
+
return `the ${error.arm} arm opened at line ${error.openedAt} is never closed with learning:end`;
|
|
201
|
+
case 'empty-arm':
|
|
202
|
+
return (`lines ${error.openedAt}-${error.closedAt}: the ${error.arm} arm holds no line with content — ` +
|
|
203
|
+
`delete the arm or put its text inside it`);
|
|
204
|
+
case 'stray-marker':
|
|
205
|
+
return (`line ${error.line}: "${error.text}" names a learning marker but is not one — a marker ` +
|
|
206
|
+
`is a whole line, exactly <!-- learning:on -->, <!-- learning:off --> or <!-- learning:end -->`);
|
|
207
|
+
default: {
|
|
208
|
+
const unhandled = error;
|
|
209
|
+
return `unhandled learning split error ${JSON.stringify(unhandled)}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=learning-variants.js.map
|
package/dist/core/manifest.js
CHANGED
|
@@ -213,6 +213,68 @@ export async function syncManifestFeature(devflowDir, key, value) {
|
|
|
213
213
|
manifest.updatedAt = new Date().toISOString();
|
|
214
214
|
await writeManifest(devflowDir, manifest);
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Drop `names` from a RAW parsed manifest's `plugins` list. Pure — returns a new
|
|
218
|
+
* object and never mutates its input. Null when there is nothing to write: the
|
|
219
|
+
* value is not a manifest-shaped object, or none of `names` is listed.
|
|
220
|
+
*
|
|
221
|
+
* Only `plugins` and `updatedAt` change; every other key is carried verbatim,
|
|
222
|
+
* `knownPlugins` included. That snapshot records which registry plugins the last
|
|
223
|
+
* install knew, and init adopts a registry plugin that is in neither list as new
|
|
224
|
+
* (`resolveSeedPlugins`), so dropping a removed plugin from it would make the next
|
|
225
|
+
* re-init install it again. Going through readManifest()/writeManifest() instead
|
|
226
|
+
* would drop every key ManifestData does not model (one a newer devflow wrote,
|
|
227
|
+
* say) and persist that reader's unrelated heals as a side effect of a removal.
|
|
228
|
+
* Entries that are not strings are neither matched nor dropped.
|
|
229
|
+
*/
|
|
230
|
+
export function withoutManifestPlugins(rawManifest, names, now) {
|
|
231
|
+
if (typeof rawManifest !== 'object' || rawManifest === null || Array.isArray(rawManifest))
|
|
232
|
+
return null;
|
|
233
|
+
const record = rawManifest;
|
|
234
|
+
if (!Array.isArray(record.plugins))
|
|
235
|
+
return null;
|
|
236
|
+
const doomed = new Set(names);
|
|
237
|
+
const kept = record.plugins.filter(entry => !(typeof entry === 'string' && doomed.has(entry)));
|
|
238
|
+
if (kept.length === record.plugins.length)
|
|
239
|
+
return null;
|
|
240
|
+
return { ...record, plugins: kept, updatedAt: now };
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* D-UNINSTALL-DROPS-PLUGIN: record a selective uninstall in `<devflowDir>/manifest.json`.
|
|
244
|
+
*
|
|
245
|
+
* The manifest is the install record init seeds from (a re-init keeps the prior
|
|
246
|
+
* selection), so a plugin `devflow uninstall --plugin` removed but the manifest
|
|
247
|
+
* still lists would come back on the next plain `devflow init`. This is the one
|
|
248
|
+
* writer that takes a name off that list (atomic temp + rename).
|
|
249
|
+
*
|
|
250
|
+
* A quiet no-op, not an error, when there is nothing to record: no manifest (a
|
|
251
|
+
* pre-manifest install), one that is not valid JSON or not manifest-shaped, or
|
|
252
|
+
* none of `names` listed — the file is left byte-identical. Never throws: a failed
|
|
253
|
+
* write comes back as `{ ok: false }` for the caller to report.
|
|
254
|
+
*/
|
|
255
|
+
export async function removeManifestPlugins(devflowDir, names) {
|
|
256
|
+
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
257
|
+
let raw;
|
|
258
|
+
try {
|
|
259
|
+
raw = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// ENOENT, EACCES or SyntaxError — no usable manifest, so nothing to record.
|
|
263
|
+
return { ok: true, removed: [] };
|
|
264
|
+
}
|
|
265
|
+
const next = withoutManifestPlugins(raw, names, new Date().toISOString());
|
|
266
|
+
if (next === null)
|
|
267
|
+
return { ok: true, removed: [] };
|
|
268
|
+
try {
|
|
269
|
+
await writeFileAtomicExclusive(manifestPath, JSON.stringify(next, null, 2) + '\n');
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
273
|
+
}
|
|
274
|
+
const doomed = new Set(names);
|
|
275
|
+
const before = raw.plugins;
|
|
276
|
+
return { ok: true, removed: before.filter((entry) => typeof entry === 'string' && doomed.has(entry)) };
|
|
277
|
+
}
|
|
216
278
|
/**
|
|
217
279
|
* Merge new plugins into existing plugin list (union, no duplicates).
|
|
218
280
|
* Preserves order: existing plugins first, then new ones appended.
|
|
@@ -120,6 +120,13 @@ export function validateContractOutputName(name) {
|
|
|
120
120
|
* from here keeps the table below the only place a destination is spelled.
|
|
121
121
|
*/
|
|
122
122
|
export const AGENTS_OUTPUT_DIR = 'dist/agents';
|
|
123
|
+
/**
|
|
124
|
+
* Repo-relative destination for `commands` hosts.
|
|
125
|
+
*
|
|
126
|
+
* Exported for the same reason as AGENTS_OUTPUT_DIR: the orphan prune names the
|
|
127
|
+
* directory even when no command host is planned.
|
|
128
|
+
*/
|
|
129
|
+
export const COMMANDS_OUTPUT_DIR = 'dist/commands';
|
|
123
130
|
/**
|
|
124
131
|
* The bare (unprefixed) skill that OWNS the generated references.
|
|
125
132
|
*
|
|
@@ -153,7 +160,7 @@ export const SKILL_REFS_SKILL_NAME = 'git';
|
|
|
153
160
|
*/
|
|
154
161
|
export const SKILL_REFS_OUTPUT_DIR = `dist/skills/${SKILL_REFS_SKILL_NAME}/references`;
|
|
155
162
|
const ALLOWED_OUTPUT_DIRS = [
|
|
156
|
-
{ dir:
|
|
163
|
+
{ dir: COMMANDS_OUTPUT_DIR, variant: 'commands' },
|
|
157
164
|
{ dir: AGENTS_OUTPUT_DIR, variant: 'agents' },
|
|
158
165
|
{ dir: SKILL_REFS_OUTPUT_DIR, variant: 'skill-refs' },
|
|
159
166
|
];
|
|
@@ -363,6 +370,30 @@ export const GIT_CROSS_CUTTING_DOCS = [
|
|
|
363
370
|
* Registering a provider whose `subdir` is one of MCP_BACKED_PROVIDER_SUBDIRS is
|
|
364
371
|
* also what opens the generation gate on the tool-call contract; see
|
|
365
372
|
* {@link mcpContractIsGenerated}. There is no second edit and no flag.
|
|
373
|
+
*
|
|
374
|
+
* D-TRACKER-CONTRACT-ON-DEMAND. The last entry, `_contract.mds`, is the first
|
|
375
|
+
* UNGATED `kind: 'contract'` module (the tool-call contract of
|
|
376
|
+
* {@link MCP_CONTRACT_MODULE} is gated, so it is not in this array). It emits
|
|
377
|
+
* `tracker/_contract.md`: the provider resolution and the tracker input contract
|
|
378
|
+
* that used to sit in the always-loaded Git agent, where every spawn paid for
|
|
379
|
+
* them although most spawns run a PR-host operation that never touches the
|
|
380
|
+
* issue tracker. A spawn now reads the file once, only when it runs a tracker
|
|
381
|
+
* operation, before its first tracker step. The agent's retained
|
|
382
|
+
* `## Loading the mechanics` section is the one line that names it; it keeps
|
|
383
|
+
* the PR-mechanics load rule and the merged step order, which a PR-only spawn
|
|
384
|
+
* needs and which this file cannot instruct a spawn to load. It is ungated for
|
|
385
|
+
* the reason the registry's other provider-independent files are: every install
|
|
386
|
+
* carries every provider, GitHub included, so no registered provider is the
|
|
387
|
+
* condition it depends on.
|
|
388
|
+
*
|
|
389
|
+
* The budget clause: the file is a per-SPAWN term of each tracker row (GitHub,
|
|
390
|
+
* Jira, Linear), charged the way the tool-call contract is (`contractTerm` in
|
|
391
|
+
* tests/tracker/budget-model.ts) and added in `providerLoadedSet`. It is not part
|
|
392
|
+
* of `ownLoadForProvider`, of the PR-host row or of the per-operation PR-host
|
|
393
|
+
* cap. ensure-pr-ready is both a PR-host and a tracker operation, so charging it
|
|
394
|
+
* the contract there would put it over a lower-only cap; its full cost,
|
|
395
|
+
* contract included, is priced through the tracker rows, where it is already a
|
|
396
|
+
* candidate.
|
|
366
397
|
*/
|
|
367
398
|
export const VARIANT_MODULES = [
|
|
368
399
|
{
|
|
@@ -395,6 +426,12 @@ export const VARIANT_MODULES = [
|
|
|
395
426
|
kind: 'named',
|
|
396
427
|
ops: GIT_CROSS_CUTTING_DOCS,
|
|
397
428
|
},
|
|
429
|
+
{
|
|
430
|
+
source: 'src/assets/mds/tracker/_contract.mds',
|
|
431
|
+
subdir: 'tracker',
|
|
432
|
+
kind: 'contract',
|
|
433
|
+
ops: ['_contract'],
|
|
434
|
+
},
|
|
398
435
|
];
|
|
399
436
|
// ---------------------------------------------------------------------------
|
|
400
437
|
// The tool-call contract module, and the gate on its generation
|