devflow-kit 3.2.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 +34 -0
- package/README.md +1 -1
- package/dist/agents/code.md +330 -0
- package/{src/assets → dist}/agents/design.md +11 -1
- package/{src/assets → dist}/agents/diagnose.md +17 -13
- package/dist/agents/git.md +29 -56
- package/{src/assets → dist}/agents/knowledge.md +8 -5
- package/{src/assets → dist}/agents/research.md +21 -2
- package/{src/assets → dist}/agents/review.md +18 -7
- package/{src/assets → dist}/agents/scrutinize.md +35 -11
- package/dist/agents/skim.md +148 -0
- package/{src/assets → dist}/agents/triage.md +8 -2
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/learning.js +63 -8
- package/dist/cli/commands/uninstall.js +42 -1
- package/dist/commands/bug-analysis.md +30 -8
- package/dist/commands/code-review.md +142 -61
- package/dist/commands/debug.md +15 -13
- package/dist/commands/dynamic-build.md +176 -130
- package/dist/commands/dynamic-plan.md +31 -19
- package/dist/commands/dynamic-profile.md +27 -13
- package/dist/commands/dynamic-tickets.md +28 -14
- package/dist/commands/explore.md +16 -14
- package/dist/commands/implement.md +163 -158
- 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 +61 -47
- package/dist/commands/self-review.md +50 -32
- package/dist/core/agent-models.js +53 -7
- 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-tuning-config.js +8 -0
- 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 +78 -11
- package/{src/assets → dist/learning-off}/agents/code.md +77 -28
- 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 +5 -10
- 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 +5 -1
- 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 +5 -1
- 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 +5 -1
- 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 +12 -25
- package/src/assets/agents/git.mds +29 -56
- package/src/assets/agents/knowledge.mds +125 -0
- package/src/assets/agents/learning.md +4 -6
- 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/simplify.md +20 -0
- package/src/assets/agents/skim.mds +161 -0
- package/src/assets/agents/synthesize.md +12 -0
- package/src/assets/agents/test.md +2 -2
- package/src/assets/agents/triage.mds +194 -0
- package/src/assets/agents/validate.md +15 -8
- 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 +29 -24
- package/src/assets/commands/_partials/_knowledge.mds +26 -13
- package/src/assets/commands/_partials/_plan_contract.mds +1 -1
- 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 +16 -6
- package/src/assets/commands/bug-analysis.mds +24 -2
- package/src/assets/commands/code-review.mds +148 -45
- package/src/assets/commands/debug.mds +17 -1
- package/src/assets/commands/dynamic-build.mds +144 -65
- 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 +171 -138
- 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 +69 -17
- package/src/assets/commands/self-review.mds +43 -13
- package/src/assets/mds/tracker/_common.mds +1 -1
- 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/ci-wait.cjs +636 -0
- package/src/assets/scripts/claude-md-audit.cjs +611 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +2 -3
- package/src/assets/scripts/hooks/background-memory-update +290 -9
- 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/scripts/release-trace.cjs +27 -10
- package/src/assets/skills/accessibility/SKILL.md +1 -1
- package/src/assets/skills/apply-decisions/SKILL.md +12 -82
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +9 -43
- package/src/assets/skills/architecture/SKILL.md +1 -1
- package/src/assets/skills/boundary-validation/SKILL.md +1 -1
- package/src/assets/skills/complexity/SKILL.md +1 -1
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/consistency/SKILL.md +1 -1
- package/src/assets/skills/database/SKILL.md +1 -1
- package/src/assets/skills/dependencies/SKILL.md +1 -1
- package/src/assets/skills/dependency-research/SKILL.md +3 -6
- package/src/assets/skills/design-review/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +1 -1
- package/src/assets/skills/documentation/SKILL.md +1 -1
- package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
- package/src/assets/skills/gap-analysis/SKILL.md +1 -1
- package/src/assets/skills/git/SKILL.md +1 -1
- package/src/assets/skills/go/SKILL.md +1 -1
- package/src/assets/skills/java/SKILL.md +1 -1
- package/src/assets/skills/patterns/SKILL.md +1 -1
- package/src/assets/skills/performance/SKILL.md +1 -1
- package/src/assets/skills/python/SKILL.md +1 -1
- package/src/assets/skills/qa/SKILL.md +1 -3
- package/src/assets/skills/quality-gates/SKILL.md +10 -13
- package/src/assets/skills/quality-gates/references/report-template.md +20 -20
- package/src/assets/skills/react/SKILL.md +1 -1
- package/src/assets/skills/regression/SKILL.md +1 -1
- package/src/assets/skills/reliability/SKILL.md +1 -1
- package/src/assets/skills/research-codebase/SKILL.md +1 -1
- package/src/assets/skills/research-competitor/SKILL.md +1 -1
- package/src/assets/skills/research-external/SKILL.md +1 -1
- package/src/assets/skills/research-technology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/rust/SKILL.md +1 -1
- package/src/assets/skills/security/SKILL.md +1 -1
- package/src/assets/skills/software-design/SKILL.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +9 -29
- package/src/assets/skills/testing/SKILL.md +1 -1
- package/src/assets/skills/typescript/SKILL.md +1 -1
- package/src/assets/skills/ui-design/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +3 -55
- package/src/assets/skills/worktree-support/references/discovery.md +48 -0
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
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
|
|
@@ -6,6 +6,14 @@ const VALID_MODELS = new Set(['opus', 'sonnet', 'haiku']);
|
|
|
6
6
|
function isValidModel(value) {
|
|
7
7
|
return typeof value === 'string' && VALID_MODELS.has(value);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* D-LEARNING-SHIPPED-ROW: the default model is the model the Learning agent ships
|
|
11
|
+
* with, the `model:` line of src/assets/agents/learning.md. With no layer
|
|
12
|
+
* configuring a model, the spawn directive names none and that line decides, so a
|
|
13
|
+
* different default here would describe a run that never happens.
|
|
14
|
+
* tests/decisions/config.test.ts holds this value equal to the Learning row of the
|
|
15
|
+
* agent-config table, which a change of the shipped tier edits with the frontmatter.
|
|
16
|
+
*/
|
|
9
17
|
const DEFAULTS = {
|
|
10
18
|
model: 'opus',
|
|
11
19
|
debug: false,
|
|
@@ -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
|
package/dist/core/plugins.js
CHANGED
|
@@ -64,7 +64,13 @@ export const DEVFLOW_PLUGINS = [
|
|
|
64
64
|
'security',
|
|
65
65
|
'worktree-support',
|
|
66
66
|
],
|
|
67
|
-
|
|
67
|
+
// D-CONTEXT-ECONOMY-RULE: reading discipline is a core rule, not a skill, because it
|
|
68
|
+
// must reach every session and every spawned agent without an activation step. It
|
|
69
|
+
// tells the model to list a large file's headings and read ranges, and to count
|
|
70
|
+
// search matches before printing them. Its body stays near 400 characters (ceiling
|
|
71
|
+
// 450) and names no search binary, since the rule is loaded into every session;
|
|
72
|
+
// tests/rules.test.ts enforces the length, the directives and this registration.
|
|
73
|
+
rules: ['security', 'engineering', 'quality', 'reliability', 'context-economy'],
|
|
68
74
|
},
|
|
69
75
|
{
|
|
70
76
|
name: 'devflow-plan',
|
|
@@ -244,7 +250,10 @@ export const DEVFLOW_PLUGINS = [
|
|
|
244
250
|
commands: ['/release'],
|
|
245
251
|
agents: ['git', 'validate'],
|
|
246
252
|
skills: ['git', 'worktree-support'],
|
|
247
|
-
|
|
253
|
+
// D-PRELOAD-TRIM: nothing in this plugin's corpus reads `testing` (Validate
|
|
254
|
+
// preloads only `worktree-support`), so the closure guard's reverse arm holds
|
|
255
|
+
// this list empty.
|
|
256
|
+
requires: [],
|
|
248
257
|
rules: [],
|
|
249
258
|
},
|
|
250
259
|
{
|
|
@@ -253,7 +262,9 @@ export const DEVFLOW_PLUGINS = [
|
|
|
253
262
|
commands: ['/self-review'],
|
|
254
263
|
agents: ['simplify', 'scrutinize', 'validate', 'knowledge'],
|
|
255
264
|
skills: ['quality-gates', 'software-design', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'],
|
|
256
|
-
|
|
265
|
+
// D-PRELOAD-TRIM: no agent, command or skill in this plugin's corpus names
|
|
266
|
+
// `testing` (Validate preloads only `worktree-support`), so it is not required here.
|
|
267
|
+
requires: ['apply-decisions'],
|
|
257
268
|
rules: [],
|
|
258
269
|
},
|
|
259
270
|
{
|
|
@@ -490,19 +501,52 @@ export const FEATURE_OWNED_SKILLS = ['compliance'];
|
|
|
490
501
|
* (guarded by plugins.test.ts FEATURE_OWNED constants describe block).
|
|
491
502
|
*/
|
|
492
503
|
export const FEATURE_OWNED_RULES = ['compliance'];
|
|
504
|
+
/**
|
|
505
|
+
* Skills a learning-off machine does not install.
|
|
506
|
+
*
|
|
507
|
+
* D-LEARNING-VARIANT-INSTALL: the learning-off variants of the commands and
|
|
508
|
+
* agents (D-LEARNING-VARIANTS) carry no decisions text and preload no
|
|
509
|
+
* apply-decisions, so the skill has no reader on a machine with learning off. It
|
|
510
|
+
* stays OWNED and REQUIRED by its plugins in the registry — the closure guard
|
|
511
|
+
* reasons over the learning-on variant, the superset — and the install drops it
|
|
512
|
+
* on top of the selection: `installViaFileCopy` skips it when `learning` is false
|
|
513
|
+
* and `convergeLearningVariants` installs or removes it when the switch flips.
|
|
514
|
+
*
|
|
515
|
+
* This is a machine-switch condition, not a deselection: the plan
|
|
516
|
+
* ({@link resolveSkillInstallPlan}) is untouched, so a learning-off machine never
|
|
517
|
+
* reports the skill as "removed because no selected plugin requires it" nor its
|
|
518
|
+
* shadow as inactive for a plugin that is not selected.
|
|
519
|
+
*
|
|
520
|
+
* Used by:
|
|
521
|
+
* - installer.ts installViaFileCopy: omits these from the install loop when learning is off
|
|
522
|
+
* - learning-install.ts convergeLearningVariants: installs or removes the directory
|
|
523
|
+
* - tests: independent literal ['apply-decisions'] (avoids the EXCLUDED-as-oracle trap)
|
|
524
|
+
*/
|
|
525
|
+
export const LEARNING_GATED_SKILLS = ['apply-decisions'];
|
|
526
|
+
/**
|
|
527
|
+
* A skills map with the learning-gated skills left out when learning is off.
|
|
528
|
+
*
|
|
529
|
+
* Pure: the same map instance comes back when learning is on, a copy otherwise.
|
|
530
|
+
*/
|
|
531
|
+
export function omitLearningGatedSkills(skillsMap, learning) {
|
|
532
|
+
if (learning)
|
|
533
|
+
return skillsMap;
|
|
534
|
+
const gated = LEARNING_GATED_SKILLS;
|
|
535
|
+
return new Map([...skillsMap].filter(([skill]) => !gated.includes(skill)));
|
|
536
|
+
}
|
|
493
537
|
// ── Skill-closure boundaries ──────────────────────────────────────────────────
|
|
494
538
|
/**
|
|
495
539
|
* Skills that are REFERENCED but never REQUIRED — the presence-gated set.
|
|
496
540
|
*
|
|
497
541
|
* D-PRESENCE-GATED: every language/ecosystem skill ships with an optional,
|
|
498
542
|
* command-less plugin, so a reference to one is a reference to something the
|
|
499
|
-
* user may deliberately not have. The referencing prompts are written to
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
543
|
+
* user may deliberately not have. The referencing prompts are written to
|
|
544
|
+
* proceed without it: `/code-review` spawns a language focus only when the
|
|
545
|
+
* installer stamped it into the command (D-LANGUAGE-FOCUS-STAMP,
|
|
546
|
+
* {@link installedLanguageFocuses}), and the Review and Code agents continue
|
|
547
|
+
* when the Skill invocation fails. Putting them in a `requires` would reinstate
|
|
548
|
+
* the universal install for exactly the eight skills the selection prompt
|
|
549
|
+
* exists to let a user decline (AC-25).
|
|
506
550
|
*
|
|
507
551
|
* DERIVED from the registry rather than hand-listed: a ninth language plugin is
|
|
508
552
|
* presence-gated by being declared, with no second roster to remember. Guarded
|
|
@@ -540,7 +584,7 @@ export const TEMPLATE_SKILL_REFS = [
|
|
|
540
584
|
},
|
|
541
585
|
{
|
|
542
586
|
literal: 'devflow:{FOCUS}',
|
|
543
|
-
site: 'src/assets/agents/review.
|
|
587
|
+
site: 'src/assets/agents/review.mds (the Review agent loading its own focus skill)',
|
|
544
588
|
why: 'receiving half of the same substitution — the agent is told which focus it is, not which skill exists',
|
|
545
589
|
},
|
|
546
590
|
];
|
|
@@ -728,6 +772,29 @@ export function buildScopedSkillsMap(plugins) {
|
|
|
728
772
|
}
|
|
729
773
|
return skillsMap;
|
|
730
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* The language focuses an install selection makes available — the list the
|
|
777
|
+
* installer stamps into the installed /code-review command.
|
|
778
|
+
*
|
|
779
|
+
* D-LANGUAGE-FOCUS-STAMP: the language gate moved from a run-time `test -f`
|
|
780
|
+
* probe of Claude Code's directory to this list, computed once at install time.
|
|
781
|
+
* Pure plumbing and nothing more: the registry's {@link PRESENCE_GATED_SKILLS}
|
|
782
|
+
* filtered to the skills the selection's closure installs
|
|
783
|
+
* ({@link buildScopedSkillsMap}), in registry order. Which focuses a diff gets
|
|
784
|
+
* stays a prompt rule that the orchestrator executes; no code here, or anywhere
|
|
785
|
+
* in the installer, classifies a diff or chooses a focus.
|
|
786
|
+
*
|
|
787
|
+
* The selection is the EFFECTIVE one (`FileCopyOptions.effectivePlugins`), never
|
|
788
|
+
* the plugins one run copies: `--plugin=X` installs X alone while the machine's
|
|
789
|
+
* selection is the manifest's plugins plus X, and the stamp has to describe what
|
|
790
|
+
* is on disk. Selective uninstall passes the plugins that remain.
|
|
791
|
+
*
|
|
792
|
+
* @param effectivePlugins - The plugins whose skill closure is, or stays, installed.
|
|
793
|
+
*/
|
|
794
|
+
export function installedLanguageFocuses(effectivePlugins) {
|
|
795
|
+
const installed = buildScopedSkillsMap(effectivePlugins);
|
|
796
|
+
return PRESENCE_GATED_SKILLS.filter(skill => installed.has(skill));
|
|
797
|
+
}
|
|
731
798
|
/**
|
|
732
799
|
* Build a skills map over the WHOLE registry.
|
|
733
800
|
*
|