devflow-kit 3.4.0 → 3.6.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 +127 -0
- package/README.md +12 -8
- package/dist/agents/code.md +39 -182
- package/dist/agents/{diagnose.md → debug.md} +4 -4
- package/dist/agents/design.md +0 -1
- package/dist/agents/fix.md +165 -0
- package/dist/agents/git.md +31 -693
- package/dist/agents/{scrutinize.md → harden.md} +9 -6
- package/dist/agents/knowledge.md +8 -2
- package/dist/agents/refactor.md +133 -0
- package/dist/agents/research.md +6 -0
- package/dist/agents/review.md +0 -4
- package/dist/agents/skim.md +4 -0
- package/dist/agents/triage.md +8 -1
- package/dist/cli/agents-view/render.js +1 -1
- 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 +75 -8
- package/dist/cli/commands/learning.js +12 -12
- package/dist/cli/commands/legacy-hooks.js +1 -1
- package/dist/cli/commands/rules.js +16 -0
- package/dist/cli/commands/tracker.js +112 -15
- package/dist/cli/commands/uninstall.js +111 -5
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +22 -18
- package/dist/commands/code-review.md +25 -29
- package/dist/commands/debug.md +36 -15
- package/dist/commands/dynamic-build.md +64 -78
- package/dist/commands/dynamic-plan.md +18 -14
- package/dist/commands/dynamic-profile.md +8 -6
- package/dist/commands/dynamic-tickets.md +19 -15
- package/dist/commands/explore.md +14 -14
- package/dist/commands/implement.md +122 -225
- package/dist/commands/plan.md +32 -47
- package/dist/commands/release.md +12 -7
- package/dist/commands/research.md +26 -15
- package/dist/commands/resolve.md +53 -92
- package/dist/commands/self-review.md +42 -42
- package/dist/core/agent-frontmatter.js +101 -0
- package/dist/core/agent-models.js +47 -11
- package/dist/core/compliance-compose.js +6 -4
- package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
- package/dist/core/feature-switch.js +23 -2
- package/dist/core/flags.js +111 -0
- package/dist/core/learning-queue-cleanup.js +1 -1
- package/dist/core/manifest.js +11 -3
- package/dist/core/mds-variants.js +68 -26
- package/dist/core/migrations.js +63 -44
- package/dist/core/plugins.js +49 -70
- package/dist/core/project-paths.js +3 -7
- 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/hud/codex-usage.js +247 -0
- package/dist/hud/components/codex-quota.js +37 -0
- package/dist/hud/components/usage-quota.js +1 -1
- package/dist/hud/config.js +1 -0
- package/dist/hud/index.js +16 -3
- package/dist/hud/render.js +3 -1
- package/dist/learning-off/agents/code.md +37 -180
- package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
- package/dist/learning-off/agents/design.md +0 -1
- package/dist/learning-off/agents/fix.md +161 -0
- package/dist/learning-off/agents/{scrutinize.md → harden.md} +9 -6
- package/dist/learning-off/agents/knowledge.md +8 -2
- package/dist/learning-off/agents/refactor.md +129 -0
- package/dist/learning-off/agents/research.md +6 -0
- package/dist/learning-off/agents/review.md +0 -4
- package/dist/learning-off/agents/skim.md +4 -0
- package/dist/learning-off/agents/triage.md +8 -1
- package/dist/learning-off/commands/bug-analysis.md +12 -12
- package/dist/learning-off/commands/code-review.md +14 -22
- package/dist/learning-off/commands/debug.md +24 -8
- package/dist/learning-off/commands/dynamic-build.md +58 -74
- package/dist/learning-off/commands/dynamic-plan.md +12 -10
- package/dist/learning-off/commands/dynamic-profile.md +2 -2
- package/dist/learning-off/commands/dynamic-tickets.md +13 -11
- package/dist/learning-off/commands/explore.md +3 -7
- package/dist/learning-off/commands/implement.md +110 -217
- package/dist/learning-off/commands/plan.md +21 -39
- package/dist/learning-off/commands/release.md +7 -4
- package/dist/learning-off/commands/research.md +16 -9
- package/dist/learning-off/commands/resolve.md +42 -85
- package/dist/learning-off/commands/self-review.md +30 -34
- 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 +21 -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 +17 -6
- package/dist/skills/git/references/pr/validate-branch.md +26 -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 +36 -1
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/github/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/github/setup-task.md +41 -1
- package/dist/skills/git/references/tracker/jira/associate-release.md +24 -0
- 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 +36 -1
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +41 -1
- package/dist/skills/git/references/tracker/linear/associate-release.md +24 -0
- 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 +36 -1
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +41 -1
- 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 +177 -77
- package/dist/targets/claude-code/learning-install.js +9 -1
- 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.mds +18 -222
- package/src/assets/agents/{learning.md → curate.md} +22 -72
- package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
- package/src/assets/agents/design.mds +0 -1
- package/src/assets/agents/evaluate.md +11 -5
- package/src/assets/agents/explore.md +58 -0
- package/src/assets/agents/fix.mds +113 -0
- package/src/assets/agents/git.mds +31 -693
- package/src/assets/agents/{scrutinize.mds → harden.mds} +9 -6
- package/src/assets/agents/knowledge.mds +8 -2
- package/src/assets/agents/{simplify.md → polish.md} +7 -4
- package/src/assets/agents/refactor.mds +81 -0
- package/src/assets/agents/research.mds +6 -0
- package/src/assets/agents/review.mds +0 -4
- package/src/assets/agents/skim.mds +4 -0
- package/src/assets/agents/synthesize.md +26 -352
- package/src/assets/agents/test.md +10 -2
- package/src/assets/agents/tracker.md +20 -7
- package/src/assets/agents/triage.mds +8 -1
- package/src/assets/agents/validate.md +5 -0
- package/src/assets/commands/_partials/_compliance.mds +36 -0
- package/src/assets/commands/_partials/_decisions.mds +26 -6
- package/src/assets/commands/_partials/_engine.mds +17 -17
- package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
- package/src/assets/commands/_partials/_knowledge.mds +14 -9
- package/src/assets/commands/_partials/_preamble.mds +5 -3
- package/src/assets/commands/_partials/_roster.mds +6 -5
- package/src/assets/commands/_partials/_wave.mds +2 -2
- package/src/assets/commands/bug-analysis.mds +10 -10
- package/src/assets/commands/code-review.mds +12 -22
- package/src/assets/commands/debug.mds +24 -2
- package/src/assets/commands/dynamic-build.mds +34 -51
- package/src/assets/commands/dynamic-plan.mds +4 -3
- package/src/assets/commands/dynamic-tickets.mds +5 -4
- package/src/assets/commands/explore.mds +1 -1
- package/src/assets/commands/implement.mds +108 -201
- package/src/assets/commands/plan.mds +19 -44
- package/src/assets/commands/release.mds +8 -3
- package/src/assets/commands/research.mds +14 -7
- package/src/assets/commands/resolve.mds +37 -82
- package/src/assets/commands/self-review.mds +27 -27
- 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 +45 -50
- 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 +4 -3
- package/src/assets/mds/tracker/_github.mds +60 -14
- package/src/assets/mds/tracker/_jira.mds +60 -14
- package/src/assets/mds/tracker/_linear.mds +60 -14
- package/src/assets/mds/tracker/_steps.mds +10 -9
- package/src/assets/rules/behavior.md +6 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -3
- package/src/assets/scripts/hooks/capture-prompt +1 -1
- package/src/assets/scripts/hooks/capture-question +1 -1
- package/src/assets/scripts/hooks/capture-turn +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +2 -2
- package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -10
- package/src/assets/scripts/hooks/preamble +1 -1
- package/src/assets/scripts/hooks/session-start-context +58 -45
- package/src/assets/scripts/verify-evidence.cjs +726 -9
- package/src/assets/skills/agent-references/SKILL.md +37 -0
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
- package/src/assets/skills/agent-references/references/curate/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 +6 -4
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +81 -84
- 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/gap-analysis/SKILL.md +0 -16
- package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
- package/src/assets/skills/qa/SKILL.md +1 -1
- package/src/assets/skills/quality-gates/SKILL.md +7 -7
- package/src/assets/skills/quality-gates/references/report-template.md +7 -7
- package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior rule installer for the Claude Code target.
|
|
3
|
+
*
|
|
4
|
+
* Convergence function: installs `rules/devflow/behavior.md` when the machine switch is
|
|
5
|
+
* on AND the install includes rules, and removes it in every other state.
|
|
6
|
+
*
|
|
7
|
+
* D-BEHAVIOR-OPT-IN: the behavior rule is feature-owned and OPT-IN. No plugin declares
|
|
8
|
+
* it (FEATURE_OWNED_RULES in src/core/plugins.ts), so the plugin installer never copies
|
|
9
|
+
* it; this module is its only writer. The file exists exactly when
|
|
10
|
+
* `features.behavior && features.rules`, and the same function serves every caller that
|
|
11
|
+
* can change either input: `devflow init`, `devflow behavior --enable/--disable` and
|
|
12
|
+
* `devflow rules --enable`. A full install wipes `rules/devflow/` first, and a full
|
|
13
|
+
* uninstall removes the directory, so neither needs a rule of its own.
|
|
14
|
+
*
|
|
15
|
+
* The copy goes through installRuleFile, so a shadow at ~/.devflow/rules/behavior.md
|
|
16
|
+
* overrides the shipped text the way it does for every other rule. The shadow only
|
|
17
|
+
* changes WHAT is installed; the switch still decides WHETHER.
|
|
18
|
+
*
|
|
19
|
+
* I/O orchestration in src/targets/; warn-not-throw for per-item failures, with the
|
|
20
|
+
* `warn` callback injected so callers choose how a warning surfaces.
|
|
21
|
+
*/
|
|
22
|
+
import { promises as fs } from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import { installRuleFile } from './installer.js';
|
|
25
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
26
|
+
export const BEHAVIOR_RULE_NAME = 'behavior';
|
|
27
|
+
// ── Path helpers ───────────────────────────────────────────────────────────
|
|
28
|
+
/** Installed behavior rule file: {claudeDir}/rules/devflow/behavior.md */
|
|
29
|
+
export function behaviorRulePath(claudeDir) {
|
|
30
|
+
return path.join(claudeDir, 'rules', 'devflow', `${BEHAVIOR_RULE_NAME}.md`);
|
|
31
|
+
}
|
|
32
|
+
async function pathExists(p) {
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(p);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Converge the behavior rule in the Claude Code install target.
|
|
44
|
+
*
|
|
45
|
+
* Convergence matrix:
|
|
46
|
+
* enabled + rulesEnabled → rule file installed (shipped text, or a valid shadow)
|
|
47
|
+
* enabled + !rulesEnabled → rule file removed
|
|
48
|
+
* !enabled → rule file removed
|
|
49
|
+
*
|
|
50
|
+
* Never throws. A relative `claudeDir` is refused with a warning rather than resolved
|
|
51
|
+
* against the working directory, because the removal arm deletes a path.
|
|
52
|
+
*/
|
|
53
|
+
export async function convergeBehaviorRule(opts) {
|
|
54
|
+
const { claudeDir, devflowDir, enabled, rulesEnabled, warn } = opts;
|
|
55
|
+
if (!path.isAbsolute(claudeDir)) {
|
|
56
|
+
warn(`behavior: claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
|
|
57
|
+
return { installed: false, removed: false, converged: false };
|
|
58
|
+
}
|
|
59
|
+
const target = behaviorRulePath(claudeDir);
|
|
60
|
+
if (enabled && rulesEnabled) {
|
|
61
|
+
try {
|
|
62
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
63
|
+
const outcome = await installRuleFile(BEHAVIOR_RULE_NAME, devflowDir, path.dirname(target));
|
|
64
|
+
if (outcome === 'skipped') {
|
|
65
|
+
warn('behavior: rule install failed — the file could not be copied');
|
|
66
|
+
return { installed: false, removed: false, converged: false };
|
|
67
|
+
}
|
|
68
|
+
if (outcome === 'source-invalid-shadow:empty-shadow-file' || outcome === 'source-invalid-shadow:not-a-file') {
|
|
69
|
+
warn("behavior: the shadow is invalid — Devflow's version was installed");
|
|
70
|
+
return { installed: true, removed: false, converged: false };
|
|
71
|
+
}
|
|
72
|
+
return { installed: true, removed: false, converged: true };
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
warn(`behavior: rule install failed — ${String(err)}`);
|
|
76
|
+
return { installed: false, removed: false, converged: false };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Off, or rules off: no rule. Probe first so the caller can report a removal;
|
|
80
|
+
// absence is already the desired end state.
|
|
81
|
+
if (!(await pathExists(target)))
|
|
82
|
+
return { installed: false, removed: false, converged: true };
|
|
83
|
+
try {
|
|
84
|
+
await fs.rm(target, { force: true });
|
|
85
|
+
return { installed: false, removed: true, converged: true };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
warn(`behavior: failed to remove rule — ${String(err)}`);
|
|
89
|
+
return { installed: true, removed: false, converged: false };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ── Manifest-slice wrapper ─────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Thin wrapper around convergeBehaviorRule that takes its inputs from a manifest slice
|
|
95
|
+
* in one place, so the call sites (init.ts, rules.ts, behavior.ts) do not each hand-assemble them.
|
|
96
|
+
*
|
|
97
|
+
* @param opts.rulesEnabledOverride - When provided, replaces manifest.features.rules as
|
|
98
|
+
* the rulesEnabled source. Used by rules.ts, where rulesEnabled reflects the actual
|
|
99
|
+
* install outcome rather than the value stored in the manifest.
|
|
100
|
+
*/
|
|
101
|
+
export async function convergeBehaviorFromManifest(opts) {
|
|
102
|
+
const { claudeDir, devflowDir, manifest, warn, rulesEnabledOverride } = opts;
|
|
103
|
+
return convergeBehaviorRule({
|
|
104
|
+
claudeDir,
|
|
105
|
+
devflowDir,
|
|
106
|
+
enabled: manifest.features.behavior,
|
|
107
|
+
rulesEnabled: rulesEnabledOverride ?? manifest.features.rules,
|
|
108
|
+
warn,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=behavior-install.js.map
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { SKILL_NAMESPACE, prefixSkillName, unprefixSkillName, getAllSkillNames, getAllAgentNames, getAllCommandNames, FEATURE_OWNED_SKILLS, resolveSkillInstallPlan, omitLearningGatedSkills } from '../../core/plugins.js';
|
|
4
|
+
import { SKILL_NAMESPACE, buildAssetMaps, prefixSkillName, unprefixSkillName, getAllSkillNames, getAllAgentNames, getAllCommandNames, FEATURE_OWNED_SKILLS, resolveSkillInstallPlan, omitLearningGatedSkills } from '../../core/plugins.js';
|
|
5
5
|
import { skillsDir, agentSourceDirs, rulesDir, commandSourceDirs, scriptsDir, compiledSkillRefsDir } from '../../core/assets.js';
|
|
6
6
|
import { getPackageRoot, isContainedIn } from '../../core/paths.js';
|
|
7
7
|
import { sweepOrphanedAssets, mdFileName, mdEntryName } from '../../core/orphan-sweep.js';
|
|
8
8
|
import { generatedReferenceManifest, installedReferenceManifest, PR_HOST_DESTINATION_ROOT, SKILL_REFS_SKILL_NAME, TRACKER_DESTINATION_ROOT } from '../../core/mds-variants.js';
|
|
9
9
|
import { sweepOrphanedReferences, MAX_REFERENCE_SWEEP_DEPTH } from '../../core/reference-sweep.js';
|
|
10
10
|
import { TRACKER_AGENT_NAME } from './tracker-install.js';
|
|
11
|
+
import { isTrackerStampedAgent } from './tracker-servers.js';
|
|
12
|
+
import { stampAgentTools } from '../../core/agent-frontmatter.js';
|
|
13
|
+
import { TRACKER_MCP_SERVERS_DEFAULT, trackerServerTools } from '../../core/tracker.js';
|
|
14
|
+
import { BUILTIN_OVERRIDE_AGENTS, findForeignAgentOwner } from './agent-type-owner.js';
|
|
11
15
|
import { restampInstalledCommands } from './language-stamp.js';
|
|
12
16
|
// ---------------------------------------------------------------------------
|
|
13
17
|
// Shadow validation helpers (exported — reused by Step 4 list commands)
|
|
@@ -1264,6 +1268,61 @@ export async function composeScripts(scriptsTarget) {
|
|
|
1264
1268
|
}
|
|
1265
1269
|
catch { /* already exists from prior install — leave as-is */ }
|
|
1266
1270
|
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Install one agent that carries the tracker-server stamp: the shipped source with
|
|
1273
|
+
* `servers` appended to its `tools:` line, written in one step so no copy of it ever sits on
|
|
1274
|
+
* disk unstamped (D-TRACKER-SERVER-ALLOWLIST, src/targets/claude-code/tracker-servers.ts).
|
|
1275
|
+
*
|
|
1276
|
+
* A source that cannot take the stamp is installed as shipped and reported: an install must
|
|
1277
|
+
* not die on a stamp, and the unstamped agent then reaches no tracker server, which
|
|
1278
|
+
* `devflow tracker --servers` or a re-run of `devflow init` repairs.
|
|
1279
|
+
*/
|
|
1280
|
+
async function writeStampedAgent(srcFile, target, servers, warn) {
|
|
1281
|
+
const source = await fs.readFile(srcFile, 'utf-8');
|
|
1282
|
+
const stamped = stampAgentTools(source, trackerServerTools(servers));
|
|
1283
|
+
if (!stamped.ok) {
|
|
1284
|
+
warn(`tracker servers: ${path.basename(target)} could not be stamped (${stamped.error}); installed without tracker servers`);
|
|
1285
|
+
await fs.copyFile(srcFile, target);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
await fs.writeFile(target, stamped.value.content);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* D-EXPLORE-NO-CLOBBER, install half: true when `agentName` takes over a built-in
|
|
1292
|
+
* agent type (Explore) and the user already owns an agent of that type under
|
|
1293
|
+
* `{claudeDir}/agents`, so devflow's copy must not be installed.
|
|
1294
|
+
*
|
|
1295
|
+
* The user's file is only ever read. A copy of devflow's own left by an earlier install
|
|
1296
|
+
* is removed from `agentsTarget`, since two user-level agents of one type fall back to
|
|
1297
|
+
* file load order and the stale one could win. Every outcome that is not a conflict
|
|
1298
|
+
* installs: an unreadable agents directory warns and installs rather than withholding
|
|
1299
|
+
* the override on a guess. Warn-not-throw, so it never aborts an install.
|
|
1300
|
+
*/
|
|
1301
|
+
async function skipsForUserOwnedType(agentName, claudeDir, agentsTarget, warn) {
|
|
1302
|
+
const agentType = BUILTIN_OVERRIDE_AGENTS.get(agentName);
|
|
1303
|
+
if (agentType === undefined)
|
|
1304
|
+
return false;
|
|
1305
|
+
const scan = await findForeignAgentOwner({
|
|
1306
|
+
agentsRoot: path.join(claudeDir, 'agents'),
|
|
1307
|
+
ownDir: agentsTarget,
|
|
1308
|
+
agentType,
|
|
1309
|
+
});
|
|
1310
|
+
if (!scan.ok) {
|
|
1311
|
+
warn(`Could not check ${scan.error.dir} for an existing ${agentType} agent (${scan.error.reason}); installing devflow's ${agentType} agent`);
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
if (scan.value === undefined)
|
|
1315
|
+
return false;
|
|
1316
|
+
try {
|
|
1317
|
+
await fs.rm(path.join(agentsTarget, mdFileName(agentName)), { force: true });
|
|
1318
|
+
}
|
|
1319
|
+
catch (err) {
|
|
1320
|
+
warn(`Could not remove devflow's earlier ${agentType} agent — ${String(err)}`);
|
|
1321
|
+
}
|
|
1322
|
+
warn(`Skipped devflow's ${agentType} agent: ${scan.value} already defines ${agentType}, and Claude Code keys agents by name. ` +
|
|
1323
|
+
`That file is left untouched; delete or rename it and re-run \`devflow init\` to use devflow's.`);
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1267
1326
|
/**
|
|
1268
1327
|
* First path in `candidates` that exists on disk, or undefined when none do.
|
|
1269
1328
|
* Bounded by candidates.length. The fs.access rejection is the existence probe,
|
|
@@ -1373,6 +1432,90 @@ async function removeDeselectedSkills(skillsDir, names) {
|
|
|
1373
1432
|
}
|
|
1374
1433
|
return { removed, failed };
|
|
1375
1434
|
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Write the commands and the agents of `plugins` into `claudeDir`, and nothing else:
|
|
1437
|
+
* no sweep, no stamp, no skills. The one copy path shared by `installViaFileCopy` and
|
|
1438
|
+
* the selective uninstall, which rewrites the files of the plugins that stay installed
|
|
1439
|
+
* after it removed a retired agent.
|
|
1440
|
+
*
|
|
1441
|
+
* Commands come from dist/commands/{name}.md, or — with learning off — the learning-off
|
|
1442
|
+
* variant dist/learning-off/commands/{name}.md when the host has one
|
|
1443
|
+
* (D-LEARNING-VARIANT-INSTALL; commandSourceDirs owns the order). A declared command
|
|
1444
|
+
* with no compiled source file is a hard error, not a skip.
|
|
1445
|
+
*
|
|
1446
|
+
* Agents resolve dist-first with a src fallback: dist/agents/{name}.md (compiled from an
|
|
1447
|
+
* .mds generator host) wins over src/assets/agents/{name}.md. A declared agent absent from
|
|
1448
|
+
* BOTH is a build/packaging failure and throws rather than silently skipping (matches the
|
|
1449
|
+
* command pattern); the message names the build step as well as the tree.
|
|
1450
|
+
*
|
|
1451
|
+
* D-TRACKER-AGENT-OWNER: every declared agent but ONE. The Tracker agent is converged by
|
|
1452
|
+
* `convergeTrackerArtifacts` alone (plan A3), which runs after the install in init and
|
|
1453
|
+
* reports whether this run wrote it. Copying it here too would make every install do the
|
|
1454
|
+
* work twice, and a fresh install would never report `installed` because converge would
|
|
1455
|
+
* find this loop's byte-identical copy already in place. The name is skipped from the COPY
|
|
1456
|
+
* set only. It stays declared in `devflow-core-skills.agents`, so the agent sweep — which
|
|
1457
|
+
* keys on the full registry via getAllAgentNames() — still treats a converged tracker.md as
|
|
1458
|
+
* known and leaves it alone.
|
|
1459
|
+
*/
|
|
1460
|
+
export async function installCommandsAndAgents(opts) {
|
|
1461
|
+
const { plugins, agentsMap, claudeDir, learning, trackerServers, warn } = opts;
|
|
1462
|
+
const commandsTarget = path.join(claudeDir, 'commands', 'devflow');
|
|
1463
|
+
const commandDirs = commandSourceDirs(learning);
|
|
1464
|
+
const commandsSourceNames = new Set();
|
|
1465
|
+
for (const plugin of plugins) {
|
|
1466
|
+
for (const cmd of plugin.commands) {
|
|
1467
|
+
const name = cmd.startsWith('/') ? cmd.slice(1) : cmd;
|
|
1468
|
+
commandsSourceNames.add(name);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (commandsSourceNames.size > 0) {
|
|
1472
|
+
await fs.mkdir(commandsTarget, { recursive: true });
|
|
1473
|
+
for (const name of commandsSourceNames) {
|
|
1474
|
+
const srcFile = await firstExisting(commandDirs.map(dir => path.join(dir, mdFileName(name))));
|
|
1475
|
+
if (srcFile === undefined) {
|
|
1476
|
+
// The error names the learning-ON path: that file exists for every declared
|
|
1477
|
+
// command, while a learning-off file exists only for a host with an arm.
|
|
1478
|
+
throw new Error(`Command source not found for declared command "${name}": ${path.join(commandSourceDirs(true)[0], mdFileName(name))}. ` +
|
|
1479
|
+
`Ensure build:mds ran successfully before install.`);
|
|
1480
|
+
}
|
|
1481
|
+
await fs.copyFile(srcFile, path.join(commandsTarget, mdFileName(name)));
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const agentsTarget = path.join(claudeDir, 'agents', 'devflow');
|
|
1485
|
+
// Learning-aware (D-LEARNING-VARIANT-INSTALL). An injected list wins as given.
|
|
1486
|
+
const agentDirs = opts.agentSourceDirs ?? agentSourceDirs(undefined, learning);
|
|
1487
|
+
const allAgentNames = new Set();
|
|
1488
|
+
for (const plugin of plugins) {
|
|
1489
|
+
for (const agent of plugin.agents) {
|
|
1490
|
+
if (agent === TRACKER_AGENT_NAME)
|
|
1491
|
+
continue;
|
|
1492
|
+
if (!allAgentNames.has(agent) && agentsMap.get(agent) === plugin.name) {
|
|
1493
|
+
allAgentNames.add(agent);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
if (allAgentNames.size > 0) {
|
|
1498
|
+
await fs.mkdir(agentsTarget, { recursive: true });
|
|
1499
|
+
for (const agentName of allAgentNames) {
|
|
1500
|
+
if (await skipsForUserOwnedType(agentName, claudeDir, agentsTarget, warn))
|
|
1501
|
+
continue;
|
|
1502
|
+
const candidates = agentDirs.map(dir => path.join(dir, mdFileName(agentName)));
|
|
1503
|
+
const srcFile = await firstExisting(candidates);
|
|
1504
|
+
if (srcFile === undefined) {
|
|
1505
|
+
throw new Error(`Agent source not found for declared agent "${agentName}": ${path.join(opts.agentSourceDirs?.[0] ?? agentSourceDirs()[0], mdFileName(agentName))}. ` +
|
|
1506
|
+
`Run \`npm run build:mds\` if it is compiled from an .mds generator host, otherwise ` +
|
|
1507
|
+
`ensure the agent file exists in src/assets/agents/ (searched: ${candidates.join(', ')}).`);
|
|
1508
|
+
}
|
|
1509
|
+
const agentTarget = path.join(agentsTarget, mdFileName(agentName));
|
|
1510
|
+
if (isTrackerStampedAgent(agentName)) {
|
|
1511
|
+
await writeStampedAgent(srcFile, agentTarget, trackerServers, warn);
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
await fs.copyFile(srcFile, agentTarget);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1376
1519
|
/**
|
|
1377
1520
|
* Install plugins via manual file copy.
|
|
1378
1521
|
* Handles cleanup of old monolithic structure, deduplication of shared assets,
|
|
@@ -1381,7 +1524,7 @@ async function removeDeselectedSkills(skillsDir, names) {
|
|
|
1381
1524
|
* Returns an InstallReport describing which shadows were applied and which were skipped.
|
|
1382
1525
|
*/
|
|
1383
1526
|
export async function installViaFileCopy(options) {
|
|
1384
|
-
const { plugins, claudeDir, devflowDir, skillsMap, agentsMap, rulesMap = new Map(), isPartialInstall, spinner, learning, warn = () => { }, } = options;
|
|
1527
|
+
const { plugins, claudeDir, devflowDir, skillsMap, agentsMap, rulesMap = new Map(), isPartialInstall, spinner, learning, trackerServers = TRACKER_MCP_SERVERS_DEFAULT, warn = () => { }, } = options;
|
|
1385
1528
|
const report = {
|
|
1386
1529
|
shadowedSkills: [],
|
|
1387
1530
|
shadowedRules: [],
|
|
@@ -1517,38 +1660,42 @@ export async function installViaFileCopy(options) {
|
|
|
1517
1660
|
for (const failure of deselected.failed) {
|
|
1518
1661
|
warn(`Could not remove deselected skill "${prefixSkillName(failure.name)}" — ${String(failure.error)}`);
|
|
1519
1662
|
}
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1663
|
+
// Retired agents. The sweep runs BEFORE the copy so the copy can see what it found:
|
|
1664
|
+
// an agent file whose name left the registry means a previous release is installed
|
|
1665
|
+
// here, and every command of that release still spawns the retired name. Sweeping
|
|
1666
|
+
// alone would then strand the plugins that stay installed without their agents.
|
|
1667
|
+
//
|
|
1668
|
+
// Ungated: runs on every install shape. knownNames spans ALL plugins
|
|
1669
|
+
// (getAllAgentNames) so agents from uninstalled plugins survive a partial run. Only
|
|
1670
|
+
// names absent from the full registry are removed.
|
|
1671
|
+
const agentsTarget = path.join(claudeDir, 'agents', 'devflow');
|
|
1672
|
+
const agentSweep = await sweepOrphanedAssets(agentsTarget, new Set(getAllAgentNames()), mdEntryName);
|
|
1673
|
+
recordSweep(report, 'agent', agentSweep);
|
|
1674
|
+
// A partial install writes the selected plugins' commands and agents and leaves every
|
|
1675
|
+
// other plugin's files alone, which is only safe while those files still name agents
|
|
1676
|
+
// that exist. After a retired agent was removed they do not, so a partial install
|
|
1677
|
+
// writes the commands and agents of every plugin the manifest records instead. The
|
|
1678
|
+
// widening is conditional because an ordinary add-one run has nothing to repair and
|
|
1679
|
+
// must not rewrite files the user did not ask about.
|
|
1680
|
+
const widenToInstalled = isPartialInstall && agentSweep.removed.length > 0;
|
|
1681
|
+
const commandAgentPlugins = widenToInstalled ? (options.effectivePlugins ?? plugins) : plugins;
|
|
1682
|
+
const commandAgentMap = widenToInstalled ? buildAssetMaps(commandAgentPlugins).agentsMap : agentsMap;
|
|
1683
|
+
// Install commands and agents (D-TRACKER-AGENT-OWNER and the source-resolution rules
|
|
1684
|
+
// are documented on installCommandsAndAgents).
|
|
1525
1685
|
spinner.message('Installing commands and agents...');
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
1535
|
-
if (commandsSourceNames.size > 0) {
|
|
1536
|
-
await fs.mkdir(commandsTarget, { recursive: true });
|
|
1537
|
-
for (const name of commandsSourceNames) {
|
|
1538
|
-
const srcFile = await firstExisting(commandDirs.map(dir => path.join(dir, mdFileName(name))));
|
|
1539
|
-
if (srcFile === undefined) {
|
|
1540
|
-
// The error names the learning-ON path: that file exists for every declared
|
|
1541
|
-
// command, while a learning-off file exists only for a host with an arm.
|
|
1542
|
-
throw new Error(`Command source not found for declared command "${name}": ${path.join(commandSourceDirs(true)[0], mdFileName(name))}. ` +
|
|
1543
|
-
`Ensure build:mds ran successfully before install.`);
|
|
1544
|
-
}
|
|
1545
|
-
await fs.copyFile(srcFile, path.join(commandsTarget, mdFileName(name)));
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1686
|
+
await installCommandsAndAgents({
|
|
1687
|
+
plugins: commandAgentPlugins,
|
|
1688
|
+
agentsMap: commandAgentMap,
|
|
1689
|
+
claudeDir,
|
|
1690
|
+
learning,
|
|
1691
|
+
trackerServers,
|
|
1692
|
+
agentSourceDirs: options.agentSourceDirs,
|
|
1693
|
+
warn,
|
|
1694
|
+
});
|
|
1548
1695
|
// Sweep stale command files — ungated: runs on every install shape.
|
|
1549
1696
|
// knownNames spans ALL plugins (getAllCommandNames) so commands from uninstalled
|
|
1550
1697
|
// plugins survive a partial run. Only names absent from the full registry are removed.
|
|
1551
|
-
recordSweep(report, 'command', await sweepOrphanedAssets(
|
|
1698
|
+
recordSweep(report, 'command', await sweepOrphanedAssets(path.join(claudeDir, 'commands', 'devflow'), new Set(getAllCommandNames()), mdEntryName));
|
|
1552
1699
|
// D-LANGUAGE-FOCUS-STAMP: record the language focuses the EFFECTIVE selection installs on the
|
|
1553
1700
|
// installed /code-review, on every install shape. This is the language gate the command reads
|
|
1554
1701
|
// (no run-time probe of Claude Code's directory), so it runs after the copy and the sweep and
|
|
@@ -1561,53 +1708,6 @@ export async function installViaFileCopy(options) {
|
|
|
1561
1708
|
effectivePlugins: options.effectivePlugins ?? plugins,
|
|
1562
1709
|
warn,
|
|
1563
1710
|
});
|
|
1564
|
-
// Install agents (deduplicated), resolved dist-first with a src fallback:
|
|
1565
|
-
// dist/agents/{name}.md (compiled from an .mds generator host) wins over
|
|
1566
|
-
// src/assets/agents/{name}.md. A declared agent absent from BOTH is a
|
|
1567
|
-
// build/packaging failure and throws rather than silently skipping (matches
|
|
1568
|
-
// command pattern); the message names the build step as well as the tree.
|
|
1569
|
-
//
|
|
1570
|
-
// D-TRACKER-AGENT-OWNER: every declared agent but ONE. The Tracker agent is
|
|
1571
|
-
// converged by `convergeTrackerArtifacts` alone (plan A3), which runs after this
|
|
1572
|
-
// function in init and reports whether this run wrote it. Copying it here too
|
|
1573
|
-
// would make every install do the work twice, and a fresh install would never
|
|
1574
|
-
// report `installed` because converge would find this loop's byte-identical copy
|
|
1575
|
-
// already in place.
|
|
1576
|
-
//
|
|
1577
|
-
// The name is skipped from the COPY set only. It stays declared in
|
|
1578
|
-
// `devflow-core-skills.agents`, so the sweep below — which keys on the full
|
|
1579
|
-
// registry via getAllAgentNames() — still treats a converged tracker.md as known
|
|
1580
|
-
// and leaves it alone.
|
|
1581
|
-
const agentsTarget = path.join(claudeDir, 'agents', 'devflow');
|
|
1582
|
-
// Learning-aware (D-LEARNING-VARIANT-INSTALL). An injected list wins as given.
|
|
1583
|
-
const agentDirs = options.agentSourceDirs ?? agentSourceDirs(undefined, learning);
|
|
1584
|
-
const allAgentNames = new Set();
|
|
1585
|
-
for (const plugin of plugins) {
|
|
1586
|
-
for (const agent of plugin.agents) {
|
|
1587
|
-
if (agent === TRACKER_AGENT_NAME)
|
|
1588
|
-
continue;
|
|
1589
|
-
if (!allAgentNames.has(agent) && agentsMap.get(agent) === plugin.name) {
|
|
1590
|
-
allAgentNames.add(agent);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
if (allAgentNames.size > 0) {
|
|
1595
|
-
await fs.mkdir(agentsTarget, { recursive: true });
|
|
1596
|
-
for (const agentName of allAgentNames) {
|
|
1597
|
-
const candidates = agentDirs.map(dir => path.join(dir, mdFileName(agentName)));
|
|
1598
|
-
const srcFile = await firstExisting(candidates);
|
|
1599
|
-
if (srcFile === undefined) {
|
|
1600
|
-
throw new Error(`Agent source not found for declared agent "${agentName}": ${path.join(options.agentSourceDirs?.[0] ?? agentSourceDirs()[0], mdFileName(agentName))}. ` +
|
|
1601
|
-
`Run \`npm run build:mds\` if it is compiled from an .mds generator host, otherwise ` +
|
|
1602
|
-
`ensure the agent file exists in src/assets/agents/ (searched: ${candidates.join(', ')}).`);
|
|
1603
|
-
}
|
|
1604
|
-
await fs.copyFile(srcFile, path.join(agentsTarget, mdFileName(agentName)));
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
// Sweep stale agent files — ungated: runs on every install shape.
|
|
1608
|
-
// knownNames spans ALL plugins (getAllAgentNames) so agents from uninstalled
|
|
1609
|
-
// plugins survive a partial run. Only names absent from the full registry are removed.
|
|
1610
|
-
recordSweep(report, 'agent', await sweepOrphanedAssets(agentsTarget, new Set(getAllAgentNames()), mdEntryName));
|
|
1611
1711
|
// Install skills from ALL plugins (skillsMap covers all plugins, not just selected).
|
|
1612
1712
|
// Resolved from flat src/assets/skills/{name}/ (no per-plugin subdirectory).
|
|
1613
1713
|
// A declared skill whose source directory is absent is a build/packaging failure
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
import { promises as fs } from 'fs';
|
|
71
71
|
import * as path from 'path';
|
|
72
72
|
import { agentSourceDirs, commandSourceDirs, learningOffDir, } from '../../core/assets.js';
|
|
73
|
+
import { carryStampedTools } from '../../core/agent-frontmatter.js';
|
|
73
74
|
import { carryAgentOverrides, reapplyAgentMapping } from '../../core/agent-models.js';
|
|
74
75
|
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
75
76
|
import { readManifest } from '../../core/manifest.js';
|
|
@@ -161,12 +162,19 @@ async function firstReadable(candidates) {
|
|
|
161
162
|
* stays the authority: it reads agent-models.json after the converge and the carry only agrees
|
|
162
163
|
* with it (see carryAgentOverrides).
|
|
163
164
|
*
|
|
165
|
+
* D-TRACKER-SERVER-ALLOWLIST, composition: the installed Git agent carries the tracker-server
|
|
166
|
+
* entries the install stamped onto its `tools:` line, and the variant source carries none.
|
|
167
|
+
* The stamp is carried the same way (carryStampedTools), so a variant switch keeps the list
|
|
168
|
+
* and a copy already holding the right variant and its stamp compares byte-equal. Install
|
|
169
|
+
* and `devflow tracker --servers` are the only writers of the list; a converge never changes it.
|
|
170
|
+
*
|
|
164
171
|
* Pure.
|
|
165
172
|
*/
|
|
166
173
|
function withInstalledState(kind, fileName, source, installed, focuses) {
|
|
167
174
|
if (kind === 'agents') {
|
|
168
175
|
const sourceText = source.toString('utf-8');
|
|
169
|
-
const
|
|
176
|
+
const installedText = installed.toString('utf-8');
|
|
177
|
+
const carried = carryStampedTools(carryAgentOverrides(sourceText, installedText), installedText);
|
|
170
178
|
return carried === sourceText ? source : Buffer.from(carried, 'utf-8');
|
|
171
179
|
}
|
|
172
180
|
return LANGUAGE_STAMPED_COMMANDS.includes(fileName.replace(/\.md$/, ''))
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { promises as fs } from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
|
+
import { stampAgentTools } from '../../core/agent-frontmatter.js';
|
|
14
15
|
import { agentSourceDirs } from '../../core/assets.js';
|
|
15
16
|
import { mdFileName } from '../../core/orphan-sweep.js';
|
|
17
|
+
import { TRACKER_MCP_SERVERS_DEFAULT, trackerServerTools } from '../../core/tracker.js';
|
|
16
18
|
/**
|
|
17
19
|
* The agent this module owns.
|
|
18
20
|
*
|
|
@@ -50,26 +52,25 @@ async function firstExisting(candidates) {
|
|
|
50
52
|
return undefined;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
|
-
* Would
|
|
55
|
+
* Would writing `wanted` over `target` change anything?
|
|
54
56
|
*
|
|
55
|
-
* Asked once, between resolving the
|
|
57
|
+
* Asked once, between resolving the wanted bytes and writing them, so a run that would write a
|
|
56
58
|
* byte-identical copy of the installed agent reports `unchanged` instead of `installed`
|
|
57
59
|
* — the same question the reference overlay asks per unit, for the same reason: a
|
|
58
60
|
* summary that can only ever say "installed" says nothing, and a steady-state re-init
|
|
59
61
|
* announcing "tracker agent installed" is the noise this closes (QA S2).
|
|
60
62
|
*
|
|
61
|
-
* It does NOT weaken the self-heal. A hand-edited or truncated agent differs from
|
|
62
|
-
*
|
|
63
|
-
* and skipping a
|
|
63
|
+
* It does NOT weaken the self-heal. A hand-edited or truncated agent differs from what is
|
|
64
|
+
* wanted, so it is written and reported as written; only an identical file is skipped,
|
|
65
|
+
* and skipping a write of what is already there changes nothing on disk.
|
|
64
66
|
*
|
|
65
67
|
* Any error — an absent target, an unreadable one — answers "no". The fallback is the
|
|
66
|
-
*
|
|
68
|
+
* write that was going to happen anyway, so a failure to compare costs a write, never
|
|
67
69
|
* correctness.
|
|
68
70
|
*/
|
|
69
|
-
async function
|
|
71
|
+
async function writeWouldChangeNothing(wanted, target) {
|
|
70
72
|
try {
|
|
71
|
-
|
|
72
|
-
return from.equals(to);
|
|
73
|
+
return (await fs.readFile(target, 'utf-8')) === wanted;
|
|
73
74
|
}
|
|
74
75
|
catch {
|
|
75
76
|
return false;
|
|
@@ -87,11 +88,14 @@ async function copyWouldChangeNothing(source, target) {
|
|
|
87
88
|
* INERT until that directive fires, which happens only for a provider with no
|
|
88
89
|
* learned conventions.
|
|
89
90
|
*
|
|
90
|
-
*
|
|
91
|
-
* source
|
|
92
|
-
*
|
|
91
|
+
* Writes the agent in unless the installed file is already byte-identical to the
|
|
92
|
+
* source wearing the tracker-server stamp (D-TRACKER-SERVER-ALLOWLIST; the stamp is the
|
|
93
|
+
* only difference between the installed file and its source, which is why the comparison
|
|
94
|
+
* is against the STAMPED text: against the bare source the file would differ on every run).
|
|
95
|
+
* Compared rather than trusted for existing, so a truncated or hand-edited
|
|
96
|
+
* file self-heals; compared rather than re-written blind, so a run that changes
|
|
93
97
|
* nothing reports `unchanged` and the summary stays quiet
|
|
94
|
-
* (see {@link
|
|
98
|
+
* (see {@link writeWouldChangeNothing}).
|
|
95
99
|
*
|
|
96
100
|
* Never throws. A caller reads `converged`; it does not catch. The one refusal
|
|
97
101
|
* that is not an I/O degradation — a claudeDir that is not absolute — is reported
|
|
@@ -115,15 +119,28 @@ export async function convergeTrackerArtifacts(opts) {
|
|
|
115
119
|
`run \`npm run build:mds\` if it is compiled from an .mds generator host`);
|
|
116
120
|
return { converged: false, agent: 'unchanged' };
|
|
117
121
|
}
|
|
122
|
+
let wanted;
|
|
123
|
+
try {
|
|
124
|
+
const stamped = stampAgentTools(await fs.readFile(source, 'utf-8'), trackerServerTools(opts.servers ?? TRACKER_MCP_SERVERS_DEFAULT));
|
|
125
|
+
if (!stamped.ok) {
|
|
126
|
+
warn(`tracker: the Tracker agent source could not take the tracker servers (${stamped.error}) — not installed`);
|
|
127
|
+
return { converged: false, agent: 'unchanged' };
|
|
128
|
+
}
|
|
129
|
+
wanted = stamped.value.content;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
warn(`tracker: cannot read the Tracker agent source (${source}) — ${String(err)}`);
|
|
133
|
+
return { converged: false, agent: 'unchanged' };
|
|
134
|
+
}
|
|
118
135
|
// Already converged — nothing to write, and nothing for the summary to announce.
|
|
119
136
|
// The directory is not created either: an identical file at the target means it is
|
|
120
|
-
// already there (see {@link
|
|
121
|
-
if (await
|
|
137
|
+
// already there (see {@link writeWouldChangeNothing}).
|
|
138
|
+
if (await writeWouldChangeNothing(wanted, target)) {
|
|
122
139
|
return { converged: true, agent: 'unchanged' };
|
|
123
140
|
}
|
|
124
141
|
try {
|
|
125
142
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
126
|
-
await fs.
|
|
143
|
+
await fs.writeFile(target, wanted);
|
|
127
144
|
}
|
|
128
145
|
catch (err) {
|
|
129
146
|
warn(`tracker: failed to install the Tracker agent (${target}) — ${String(err)}`);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracker-server stamp for the installed Git and Tracker agents.
|
|
3
|
+
*
|
|
4
|
+
* D-TRACKER-SERVER-ALLOWLIST: the Git and Tracker agents ship a tool ALLOWLIST (their
|
|
5
|
+
* frontmatter `tools:` line), and the tracker tool servers a user has connected are
|
|
6
|
+
* configured per machine, so the shipped line names none. The installer appends one
|
|
7
|
+
* whole-server entry per configured server (`mcp__<server>`, which grants every tool the
|
|
8
|
+
* server exposes) to the INSTALLED `tools:` line of those two agents, after the file is
|
|
9
|
+
* copied. The list is `manifest.features.tracker.servers`, else TRACKER_MCP_SERVERS_DEFAULT,
|
|
10
|
+
* and `devflow tracker --servers` changes it. Decided here:
|
|
11
|
+
*
|
|
12
|
+
* - Stamped agents: Git and Tracker, the two that call tracker tools. Code and Research keep a
|
|
13
|
+
* denylist and so every tool server; Harden and Polish deny them all.
|
|
14
|
+
* - Where the stamp is written. Every path that writes an installed copy of either agent either
|
|
15
|
+
* stamps it or carries the stamp it finds:
|
|
16
|
+
* 1. `installViaFileCopy` stamps Git as it writes it (full install and `--plugin` alike);
|
|
17
|
+
* a `--plugin` run that does not install Git leaves the installed, stamped copy alone.
|
|
18
|
+
* 2. `convergeTrackerArtifacts` stamps Tracker as it writes it, and compares the STAMPED text
|
|
19
|
+
* with the installed file, so a steady-state run reports `unchanged`.
|
|
20
|
+
* 3. `convergeLearningVariants` (init, `devflow learning --enable/--disable`) carries the
|
|
21
|
+
* installed stamp onto the variant it writes (`carryStampedTools`), the way it carries
|
|
22
|
+
* `model:` and `effort:`, so a variant switch keeps the list and a repeat run is byte-equal.
|
|
23
|
+
* 4. `devflow tracker --servers` re-stamps the installed copies at once ({@link restampTrackerAgents}).
|
|
24
|
+
* `reapplyAgentMapping` and `revertExternalAgents` (`devflow agents`, the proxy, an uninstall's
|
|
25
|
+
* revert) go through `rewriteAgentFrontmatter`, which manages `model:` and `effort:` only, so
|
|
26
|
+
* they preserve the line. An agent has no shadow override: only skills and rules do.
|
|
27
|
+
* - The agent sources stay vendor-neutral. No stamped name appears under src/.
|
|
28
|
+
* - A name no connected server answers to is inert. The agent then finds no tool for the capability
|
|
29
|
+
* it needs and reports `TRACEABILITY: DEGRADED (no tracker tool for {capability})`, so a wrong
|
|
30
|
+
* or missing name is visible and the remedy is `devflow tracker --servers`.
|
|
31
|
+
*
|
|
32
|
+
* Never throws: every failure is a warning, because the install must not die on a stamp.
|
|
33
|
+
*/
|
|
34
|
+
import { promises as fs } from 'fs';
|
|
35
|
+
import * as path from 'path';
|
|
36
|
+
import { stampAgentTools } from '../../core/agent-frontmatter.js';
|
|
37
|
+
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
38
|
+
import { mdFileName } from '../../core/orphan-sweep.js';
|
|
39
|
+
import { trackerServerTools } from '../../core/tracker.js';
|
|
40
|
+
import { TRACKER_AGENT_NAME } from './tracker-install.js';
|
|
41
|
+
/** The registry name of the Git agent. */
|
|
42
|
+
export const GIT_AGENT_NAME = 'git';
|
|
43
|
+
/** The installed agents that carry the stamp. */
|
|
44
|
+
export const TRACKER_STAMPED_AGENTS = [GIT_AGENT_NAME, TRACKER_AGENT_NAME];
|
|
45
|
+
/** True when `agentName` is an agent that carries the stamp. */
|
|
46
|
+
export function isTrackerStampedAgent(agentName) {
|
|
47
|
+
return TRACKER_STAMPED_AGENTS.includes(agentName);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Stamp the installed Git and Tracker agents with `servers`.
|
|
51
|
+
*
|
|
52
|
+
* Rewrites only an installed copy (an absent one stays absent), compares against the bytes on
|
|
53
|
+
* disk at the moment of writing, and writes atomically only when they differ, so a run that
|
|
54
|
+
* changes nothing writes nothing. Never throws.
|
|
55
|
+
*/
|
|
56
|
+
export async function restampTrackerAgents(opts) {
|
|
57
|
+
const { claudeDir, servers, warn } = opts;
|
|
58
|
+
const rewritten = [];
|
|
59
|
+
const unchanged = [];
|
|
60
|
+
const absent = [];
|
|
61
|
+
const failed = [];
|
|
62
|
+
// Precondition, asserted in production code: a relative claudeDir would resolve the targets
|
|
63
|
+
// against the working directory.
|
|
64
|
+
if (!path.isAbsolute(claudeDir)) {
|
|
65
|
+
warn(`tracker servers: claudeDir is not an absolute path ("${claudeDir}") — the installed agents were not stamped`);
|
|
66
|
+
return { rewritten, unchanged, absent, failed: [...TRACKER_STAMPED_AGENTS] };
|
|
67
|
+
}
|
|
68
|
+
const tools = trackerServerTools(servers);
|
|
69
|
+
for (const agent of TRACKER_STAMPED_AGENTS) {
|
|
70
|
+
const target = path.join(claudeDir, 'agents', 'devflow', mdFileName(agent));
|
|
71
|
+
let current;
|
|
72
|
+
try {
|
|
73
|
+
current = await fs.readFile(target, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (err.code === 'ENOENT') {
|
|
77
|
+
absent.push(agent);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
warn(`tracker servers: cannot read ${mdFileName(agent)} (${target}) — ${String(err)}`);
|
|
81
|
+
failed.push(agent);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const stamped = stampAgentTools(current, tools);
|
|
86
|
+
if (!stamped.ok) {
|
|
87
|
+
warn(`tracker servers: ${mdFileName(agent)} could not be stamped (${stamped.error}), so it was left as it is. ` +
|
|
88
|
+
'Run `devflow init` to reinstall it.');
|
|
89
|
+
failed.push(agent);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!stamped.value.changed) {
|
|
93
|
+
unchanged.push(agent);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await writeFileAtomicExclusive(target, stamped.value.content);
|
|
98
|
+
rewritten.push(agent);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
warn(`tracker servers: could not rewrite ${mdFileName(agent)} (${target}) — ${String(err)}`);
|
|
102
|
+
failed.push(agent);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { rewritten, unchanged, absent, failed };
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=tracker-servers.js.map
|