devflow-kit 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -0
- package/README.md +86 -18
- package/dist/agents/git.md +824 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/attribution-prompts.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +1 -1
- package/dist/cli/commands/compliance.js +23 -1
- package/dist/cli/commands/init-seed.js +24 -26
- package/dist/cli/commands/init.js +502 -71
- package/dist/cli/commands/install-report.js +205 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +27 -37
- package/dist/cli/commands/learning.js +37 -30
- package/dist/cli/commands/memory.js +79 -69
- package/dist/cli/commands/prompt-io.js +4 -4
- package/dist/cli/commands/security.js +76 -16
- package/dist/cli/commands/skills.js +53 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +405 -0
- package/dist/cli/commands/uninstall.js +211 -65
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +22 -4
- package/dist/commands/code-review.md +44 -15
- package/dist/commands/debug.md +20 -6
- package/dist/commands/dynamic-build.md +289 -67
- package/dist/commands/dynamic-plan.md +60 -21
- package/dist/commands/dynamic-profile.md +1 -1
- package/dist/commands/dynamic-tickets.md +58 -8
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +241 -53
- package/dist/commands/plan.md +88 -17
- package/dist/commands/release.md +64 -17
- package/dist/commands/resolve.md +138 -58
- package/dist/commands/self-review.md +2 -2
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/evidence-policy.js +147 -0
- package/dist/core/feature-config.js +130 -64
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +4 -4
- package/dist/core/manifest.js +33 -7
- package/dist/core/mds-variants.js +861 -0
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +357 -9
- package/dist/core/project-paths.js +1 -1
- package/dist/core/proxy-log.js +8 -6
- package/dist/core/proxy-state.js +11 -8
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/tracker.js +407 -0
- package/dist/skills/git/references/decision-markers.md +19 -0
- package/dist/skills/git/references/learn-conventions.md +56 -0
- package/dist/skills/git/references/pr/check-ci-status.md +14 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
- package/dist/skills/git/references/pr/post-review-summary.md +42 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
- package/dist/skills/git/references/pr/validate-branch.md +18 -0
- package/dist/skills/git/references/publication-gate.md +13 -0
- package/dist/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
- package/dist/skills/git/references/tracker/github/create-release.md +11 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
- package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
- package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
- package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
- package/dist/skills/git/references/trust-rule.md +7 -0
- package/dist/targets/claude-code/installer.js +1213 -31
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +196 -74
- package/dist/targets/claude-code/tracker-install.js +161 -0
- package/package.json +4 -3
- package/src/assets/agents/code.md +42 -4
- package/src/assets/agents/design.md +1 -1
- package/src/assets/agents/git.mds +827 -0
- package/src/assets/agents/knowledge.md +1 -1
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +467 -0
- package/src/assets/agents/validate.md +7 -5
- package/src/assets/commands/_partials/_engine.mds +11 -9
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- package/src/assets/commands/_partials/_knowledge.mds +2 -2
- package/src/assets/commands/_partials/_plan_contract.mds +22 -7
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +3 -1
- package/src/assets/commands/_partials/_ticket_template.mds +3 -2
- package/src/assets/commands/_partials/_tracker.mds +18 -0
- package/src/assets/commands/_partials/_wave.mds +16 -10
- package/src/assets/commands/bug-analysis.mds +15 -5
- package/src/assets/commands/code-review.mds +34 -14
- package/src/assets/commands/debug.mds +11 -4
- package/src/assets/commands/dynamic-build.mds +227 -41
- package/src/assets/commands/dynamic-plan.mds +35 -13
- package/src/assets/commands/dynamic-tickets.mds +47 -5
- package/src/assets/commands/implement.mds +206 -52
- package/src/assets/commands/plan.mds +70 -17
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/resolve.mds +126 -56
- package/src/assets/mds/git/_pr.mds +331 -0
- package/src/assets/mds/git/_references.mds +135 -0
- package/src/assets/mds/tracker/_common.mds +156 -0
- package/src/assets/mds/tracker/_github.mds +472 -0
- package/src/assets/mds/tracker/_jira.mds +407 -0
- package/src/assets/mds/tracker/_linear.mds +449 -0
- package/src/assets/mds/tracker/_mcp.mds +299 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +14 -9
- package/src/assets/scripts/hooks/capture-prompt +6 -2
- package/src/assets/scripts/hooks/capture-question +6 -2
- package/src/assets/scripts/hooks/capture-turn +6 -2
- package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +223 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +15 -8
- package/src/assets/scripts/hooks/pre-compact-memory +12 -8
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +68 -24
- package/src/assets/scripts/hooks/session-start-context +355 -8
- package/src/assets/scripts/hooks/session-start-memory +12 -8
- package/src/assets/scripts/pr-evidence.cjs +1961 -0
- package/src/assets/scripts/redact-secrets.cjs +490 -62
- package/src/assets/scripts/release-trace.cjs +1143 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +2 -0
- package/src/assets/skills/docs-framework/SKILL.md +5 -3
- package/src/assets/skills/git/SKILL.md +8 -78
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -6
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- package/src/assets/agents/git.md +0 -938
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devflow tracker — Show or set the issue tracker provider.
|
|
3
|
+
*
|
|
4
|
+
* D-TRACKER-PAIR [DR-25]: `src/core/tracker.ts` (domain) +
|
|
5
|
+
* `src/cli/commands/tracker.ts` (CLI) mirrors the `compliance.ts` pair
|
|
6
|
+
* exactly; ADR-013's pure-core / I/O-target split is the reason both names
|
|
7
|
+
* exist. A reviewer meeting several `tracker*` files in one commit otherwise
|
|
8
|
+
* has no signal that the duplication is deliberate.
|
|
9
|
+
*
|
|
10
|
+
* Applies ADR-013: CLI-layer module; the provider domain, the strict parser and
|
|
11
|
+
* the ~/.devflow file lifecycle all live in src/core/tracker.ts.
|
|
12
|
+
* The manifest is the source of truth for the selected provider: tracker is a
|
|
13
|
+
* manifest-group feature like proxy and compliance, not
|
|
14
|
+
* .devflow/config.json-gated, so the selection is machine-wide, not per-repo.
|
|
15
|
+
* Avoids PF-015: --set converges the sentinel in BOTH directions, so flipping
|
|
16
|
+
* back to github removes what flipping away wrote.
|
|
17
|
+
* Avoids PF-009: a failed rename/rearm/sentinel step warns, it never aborts.
|
|
18
|
+
*/
|
|
19
|
+
import { Command } from 'commander';
|
|
20
|
+
import { promises as fs } from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import * as p from '@clack/prompts';
|
|
23
|
+
import color from 'picocolors';
|
|
24
|
+
import { DEFAULT_TRACKER_PROVIDER, TRACKER_ATTEMPTS_MAX, TRACKER_PROVIDERS, applyTrackerSentinel, describeTrackerValue, parseTrackerId, rearmTrackerInference, renameStaleTrackerConventions, trackerConventionsPath, } from '../../core/tracker.js';
|
|
25
|
+
import { readManifest, syncManifestFeature } from '../../core/manifest.js';
|
|
26
|
+
import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
27
|
+
import { overlayInstalledReferences, overlayUnitLabel } from '../../targets/claude-code/installer.js';
|
|
28
|
+
import { convergeTrackerArtifacts } from '../../targets/claude-code/tracker-install.js';
|
|
29
|
+
import { describeOverlayFailureState } from './install-report.js';
|
|
30
|
+
import { SKILL_REFS_SKILL_NAME, installedReferenceManifest } from '../../core/mds-variants.js';
|
|
31
|
+
import { prefixSkillName } from '../../core/plugins.js';
|
|
32
|
+
/** How many leading lines of tracker.md are scanned for frontmatter. */
|
|
33
|
+
const PROVENANCE_SCAN_LINES = 40;
|
|
34
|
+
/**
|
|
35
|
+
* How many leading BYTES of tracker.md are read.
|
|
36
|
+
*
|
|
37
|
+
* The line cap above bounds the SCAN, not the read: `String.prototype.split`'s
|
|
38
|
+
* limit truncates the resulting array once the whole file is already in memory.
|
|
39
|
+
* tracker.md is hand-editable and machine-wide, so its size is not devflow's to
|
|
40
|
+
* assume — this is the same bound the Tracker agent writes to and the Git agent
|
|
41
|
+
* loads, enforced at the one TypeScript reader (avoids PF-023: a bound is only
|
|
42
|
+
* real at the sink, and a file round-trip launders the writer's promise).
|
|
43
|
+
*/
|
|
44
|
+
const PROVENANCE_READ_BYTES = 8000;
|
|
45
|
+
/**
|
|
46
|
+
* Read at most `limit` bytes from the head of a file.
|
|
47
|
+
*
|
|
48
|
+
* `undefined` for an absent, unreadable or non-file path — the caller reports
|
|
49
|
+
* that as `absent` (PF-014: never throws).
|
|
50
|
+
*/
|
|
51
|
+
async function readBoundedHead(filePath, limit) {
|
|
52
|
+
let handle;
|
|
53
|
+
try {
|
|
54
|
+
handle = await fs.open(filePath, 'r');
|
|
55
|
+
const buffer = Buffer.alloc(limit);
|
|
56
|
+
const { bytesRead } = await handle.read(buffer, 0, limit, 0);
|
|
57
|
+
return buffer.subarray(0, bytesRead).toString('utf-8');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await handle?.close().catch(() => undefined);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read the provenance header of `~/.devflow/tracker.md`.
|
|
68
|
+
*
|
|
69
|
+
* Never throws (PF-014): an absent, unreadable, or directory path is `absent`.
|
|
70
|
+
* Only the bounded head of the file is read and only the leading frontmatter
|
|
71
|
+
* block is scanned, and nothing read here is trusted — every value is rendered
|
|
72
|
+
* through `describeTrackerValue`, because tracker.md is hand-editable and
|
|
73
|
+
* machine-wide, so its content is third-party input at every sink.
|
|
74
|
+
*/
|
|
75
|
+
export async function readTrackerProvenance(devflowDir) {
|
|
76
|
+
const head = await readBoundedHead(trackerConventionsPath(devflowDir), PROVENANCE_READ_BYTES);
|
|
77
|
+
if (head === undefined)
|
|
78
|
+
return { kind: 'absent' };
|
|
79
|
+
const lines = head.split('\n', PROVENANCE_SCAN_LINES);
|
|
80
|
+
if (lines[0]?.trim() !== '---')
|
|
81
|
+
return { kind: 'present' };
|
|
82
|
+
let provider;
|
|
83
|
+
let inferredFrom;
|
|
84
|
+
for (const line of lines.slice(1)) {
|
|
85
|
+
if (line.trim() === '---')
|
|
86
|
+
break;
|
|
87
|
+
const match = /^([A-Za-z-]+):\s*(.*)$/.exec(line);
|
|
88
|
+
if (match === null)
|
|
89
|
+
continue;
|
|
90
|
+
if (match[1] === 'provider' && provider === undefined)
|
|
91
|
+
provider = match[2].trim();
|
|
92
|
+
if (match[1] === 'inferred-from' && inferredFrom === undefined)
|
|
93
|
+
inferredFrom = match[2].trim();
|
|
94
|
+
}
|
|
95
|
+
return { kind: 'present', provider, inferredFrom };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Count the installed tracker mechanics for the resolved provider.
|
|
99
|
+
*
|
|
100
|
+
* Counts what is PRESENT against the manifest for that provider rather than
|
|
101
|
+
* listing the directory: the question a user asks `--status` is "can the agent
|
|
102
|
+
* load what it is told to load?", and a stray file in the tree is not an answer
|
|
103
|
+
* to it. Never throws (PF-014).
|
|
104
|
+
*/
|
|
105
|
+
export async function readTrackerMechanics(claudeDir, provider) {
|
|
106
|
+
const root = path.join(claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'references');
|
|
107
|
+
let present = 0;
|
|
108
|
+
for (const rel of installedReferenceManifest({ provider })) {
|
|
109
|
+
try {
|
|
110
|
+
await fs.access(path.join(root, ...rel.split('/')));
|
|
111
|
+
present++;
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const code = err.code;
|
|
115
|
+
// ENOENT is the file simply not being there — that is a count of zero for
|
|
116
|
+
// this entry, not a failure to look. Anything else IS a failure to look.
|
|
117
|
+
if (code !== undefined && code !== 'ENOENT')
|
|
118
|
+
return { kind: 'unreadable', errno: code };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return present === 0 ? { kind: 'missing' } : { kind: 'installed', count: present };
|
|
122
|
+
}
|
|
123
|
+
/** Render the mechanics state for the `--status` note. Pure. */
|
|
124
|
+
export function formatTrackerMechanics(state) {
|
|
125
|
+
switch (state.kind) {
|
|
126
|
+
case 'installed':
|
|
127
|
+
return `installed (${state.count} file(s))`;
|
|
128
|
+
case 'missing':
|
|
129
|
+
return 'MISSING — run devflow init';
|
|
130
|
+
case 'unreadable':
|
|
131
|
+
return `unreadable (${state.errno})`;
|
|
132
|
+
default: {
|
|
133
|
+
const _exhaustive = state;
|
|
134
|
+
void _exhaustive;
|
|
135
|
+
return 'unknown';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Render a provenance value for the `--status` note. Pure; bounded; sanitised. */
|
|
140
|
+
export function formatTrackerProvenance(provenance) {
|
|
141
|
+
if (provenance.kind === 'absent') {
|
|
142
|
+
return 'not present (learned in the background at a session start)';
|
|
143
|
+
}
|
|
144
|
+
const parts = ['present'];
|
|
145
|
+
if (provenance.provider !== undefined) {
|
|
146
|
+
parts.push(`provider: ${describeTrackerValue(provenance.provider)}`);
|
|
147
|
+
}
|
|
148
|
+
if (provenance.inferredFrom !== undefined) {
|
|
149
|
+
parts.push(`inferred from: ${describeTrackerValue(provenance.inferredFrom)}`);
|
|
150
|
+
}
|
|
151
|
+
return parts.join(' — ');
|
|
152
|
+
}
|
|
153
|
+
/** The real adapter — the ONE binding of each operation into the `--set` path. */
|
|
154
|
+
export function buildTrackerSetIO() {
|
|
155
|
+
return {
|
|
156
|
+
gitSkillInstalled: async (claudeDir) => {
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(path.join(claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'SKILL.md'));
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
overlayReferences: (claudeDir, provider, warn) => overlayInstalledReferences({ claudeDir, provider, warn }),
|
|
166
|
+
renameStaleConventions: renameStaleTrackerConventions,
|
|
167
|
+
syncManifest: (devflowDir, state) => syncManifestFeature(devflowDir, 'tracker', state),
|
|
168
|
+
convergeArtifacts: (claudeDir, provider, warn) => convergeTrackerArtifacts({ claudeDir, provider, warn }),
|
|
169
|
+
rearmInference: rearmTrackerInference,
|
|
170
|
+
applySentinel: applyTrackerSentinel,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Converge every tracker artifact onto a newly selected provider.
|
|
175
|
+
*
|
|
176
|
+
* D-TRACKER-CONVERGE-SET: the step order is the invariant, and it is NOT the
|
|
177
|
+
* same as `devflow init`'s — the two are different call paths obeying one
|
|
178
|
+
* principle, and forcing them through a shared signature would hide the
|
|
179
|
+
* reordering rather than document it.
|
|
180
|
+
*
|
|
181
|
+
* 1. probe `devflow:git` — the mechanics have nowhere to land without it
|
|
182
|
+
* 2. OVERLAY the references (abort on failure, exit 1)
|
|
183
|
+
* 3. rename stale conventions
|
|
184
|
+
* 4. persist the manifest
|
|
185
|
+
* 5. converge the Tracker AGENT file
|
|
186
|
+
* 6. re-arm the attempt counter
|
|
187
|
+
* 7. converge the sentinel (write gated on 5; removal unconditional)
|
|
188
|
+
*
|
|
189
|
+
* The overlay precedes the manifest write, and the agent file follows it. That
|
|
190
|
+
* asymmetry is the point: the reference subtree is INERT — nothing loads it
|
|
191
|
+
* until a Git spawn resolves a provider, and resolving a provider reads the
|
|
192
|
+
* manifest — so installing it early costs nothing and lets a failure abort
|
|
193
|
+
* cleanly with the previous provider still whole. The agent file and the
|
|
194
|
+
* sentinel are ADVERTISING artifacts: they announce a provider, so they must
|
|
195
|
+
* never get ahead of the manifest that records it.
|
|
196
|
+
*
|
|
197
|
+
* Both abort branches — an absent `devflow:git` and a failed overlay — leave the
|
|
198
|
+
* SAME end state and exit 1 (design review C3): manifest, sentinel and
|
|
199
|
+
* conventions unchanged. What the overlay branch cannot claim is that nothing
|
|
200
|
+
* moved at all: the overlay is atomic PER UNIT, so units that succeeded are
|
|
201
|
+
* converged and only the failed ones are reported (design review H1). Saying
|
|
202
|
+
* "nothing else changed" would be the more comfortable sentence and the false
|
|
203
|
+
* one.
|
|
204
|
+
*
|
|
205
|
+
* Never throws, except where its callee does: an absent generated tree is a
|
|
206
|
+
* build artifact that was never produced, and the CLI turns that into exit 1
|
|
207
|
+
* with the build command named — asymmetric against the warn-only rename,
|
|
208
|
+
* re-arm and sentinel steps, because those degrade a working install while an
|
|
209
|
+
* absent tree means there is nothing to install at all.
|
|
210
|
+
*/
|
|
211
|
+
export async function runTrackerSet(opts) {
|
|
212
|
+
const { devflowDir, claudeDir, current, requested, io } = opts;
|
|
213
|
+
const messages = [];
|
|
214
|
+
const abort = (text) => {
|
|
215
|
+
messages.push({ level: 'error', text });
|
|
216
|
+
return { exitCode: 1, provider: current.provider, messages };
|
|
217
|
+
};
|
|
218
|
+
// 1. Without devflow:git there is no references/ directory to converge into,
|
|
219
|
+
// and creating one would leave an invisible husk under a skill that does
|
|
220
|
+
// not exist — a directory no sweep looks inside because no skill claims it.
|
|
221
|
+
if (!await io.gitSkillInstalled(claudeDir)) {
|
|
222
|
+
return abort(`devflow:git is not installed — run devflow init --tracker ${requested}. ` +
|
|
223
|
+
`The manifest, sentinel and conventions file are unchanged.`);
|
|
224
|
+
}
|
|
225
|
+
// 2. The overlay. Runs even when the provider is unchanged, so a `--set` that
|
|
226
|
+
// repeats the current selection self-heals a damaged subtree instead of
|
|
227
|
+
// early-returning on an equality check that proves nothing about disk.
|
|
228
|
+
const overlayWarnings = [];
|
|
229
|
+
let overlay;
|
|
230
|
+
try {
|
|
231
|
+
overlay = await io.overlayReferences(claudeDir, requested, (msg) => overlayWarnings.push(msg));
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
return abort(`Tracker: not changed — ${requested} assets could not be installed. ` +
|
|
235
|
+
`${error instanceof Error ? error.message : String(error)}`);
|
|
236
|
+
}
|
|
237
|
+
for (const text of overlayWarnings)
|
|
238
|
+
messages.push({ level: 'warn', text });
|
|
239
|
+
if (overlay.overlayFailures.length > 0) {
|
|
240
|
+
for (const failure of overlay.overlayFailures) {
|
|
241
|
+
messages.push({
|
|
242
|
+
level: 'warn',
|
|
243
|
+
text: `${overlayUnitLabel(failure.unit)}: ${failure.error} — ` +
|
|
244
|
+
`${describeOverlayFailureState(failure.state)}`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return abort(`Tracker: not changed — ${requested} assets could not be installed. The manifest, ` +
|
|
248
|
+
`sentinel and conventions file are unchanged; the overlay is atomic per unit, so the ` +
|
|
249
|
+
`units that succeeded are converged and the ${overlay.overlayFailures.length} that failed ` +
|
|
250
|
+
`are reported above.`);
|
|
251
|
+
}
|
|
252
|
+
// 3. A conventions file inferred for the previous provider is stale the moment
|
|
253
|
+
// the provider changes — move it aside so it can never be silently
|
|
254
|
+
// authoritative, and so the reader-side mismatch guard has nothing to fight.
|
|
255
|
+
const transition = await io.renameStaleConventions(devflowDir, current.provider, requested);
|
|
256
|
+
if (transition.kind === 'renamed') {
|
|
257
|
+
messages.push({
|
|
258
|
+
level: 'info',
|
|
259
|
+
text: `Moved the previous ${transition.previous} conventions aside: ${transition.to}`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else if (transition.kind === 'failed') {
|
|
263
|
+
messages.push({ level: 'warn', text: transition.error });
|
|
264
|
+
}
|
|
265
|
+
// 4. Persist. Everything after this point advertises the persisted value.
|
|
266
|
+
await io.syncManifest(devflowDir, { provider: requested });
|
|
267
|
+
// 5. The Tracker agent file.
|
|
268
|
+
const agentWarnings = [];
|
|
269
|
+
const artifacts = await io.convergeArtifacts(claudeDir, requested, (msg) => agentWarnings.push(msg));
|
|
270
|
+
for (const text of agentWarnings)
|
|
271
|
+
messages.push({ level: 'warn', text });
|
|
272
|
+
// 6 + 7. [DR-22] a selection change re-arms the attempt counter; [DR-10] the
|
|
273
|
+
// sentinel converges in both directions. A removal is always attempted, because
|
|
274
|
+
// a stale sentinel costs every future session a fork for a provider the user
|
|
275
|
+
// has left; the WRITE is gated on the agent being SPAWNABLE (design review C2).
|
|
276
|
+
//
|
|
277
|
+
// The gate is `agentPresent`, not `converged`. Reading `converged` alone is
|
|
278
|
+
// wrong in both directions: a re-copy that fails over an already-installed
|
|
279
|
+
// agent would disable a provider that still works, and merely SUPPRESSING the
|
|
280
|
+
// write leaves the previous provider's sentinel in place — so a jira → linear
|
|
281
|
+
// switch whose agent copy failed keeps advertising jira, which is the very
|
|
282
|
+
// state the suppression exists to prevent. Not-spawnable therefore REMOVES,
|
|
283
|
+
// through the one sentinel owner in src/core/tracker.ts (D-TRACKER-OWNER) —
|
|
284
|
+
// never an inline fs.rm here.
|
|
285
|
+
const rearm = await io.rearmInference(devflowDir);
|
|
286
|
+
if (!rearm.ok)
|
|
287
|
+
messages.push({ level: 'warn', text: rearm.error });
|
|
288
|
+
const advertisable = requested === DEFAULT_TRACKER_PROVIDER || artifacts.agentPresent;
|
|
289
|
+
const sentinel = await io.applySentinel(devflowDir, advertisable ? requested : DEFAULT_TRACKER_PROVIDER);
|
|
290
|
+
if (!sentinel.ok)
|
|
291
|
+
messages.push({ level: 'warn', text: sentinel.error });
|
|
292
|
+
if (!advertisable) {
|
|
293
|
+
messages.push({
|
|
294
|
+
level: 'warn',
|
|
295
|
+
text: `Tracker sentinel removed — no ${requested} agent is installed, so nothing advertises a ` +
|
|
296
|
+
`provider whose agent is missing and no session will try to spawn it. ` +
|
|
297
|
+
`Re-run devflow tracker --set ${requested}.`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const removed = overlay.pruned.removed.length;
|
|
301
|
+
const agentNote = artifacts.agent === 'unchanged' ? '' : `, tracker agent ${artifacts.agent}`;
|
|
302
|
+
const moved = overlay.overlaidRefs.length > 0 || removed > 0 || agentNote !== '';
|
|
303
|
+
messages.push({
|
|
304
|
+
level: requested === current.provider ? 'info' : 'success',
|
|
305
|
+
text: moved
|
|
306
|
+
? `Tracker: ${requested} — ${overlay.overlaidRefs.length} installed, ${removed} removed${agentNote}`
|
|
307
|
+
: `Tracker: ${requested} (unchanged)`,
|
|
308
|
+
});
|
|
309
|
+
return { exitCode: 0, provider: requested, messages };
|
|
310
|
+
}
|
|
311
|
+
export const trackerCommand = new Command('tracker')
|
|
312
|
+
.description('Show or set the issue tracker provider')
|
|
313
|
+
.option('--status', 'Show the selected provider and the learned conventions file, and re-arm background inference')
|
|
314
|
+
.option('--set <id>', 'Set the issue tracker provider: github, jira, or linear')
|
|
315
|
+
.action(async (options) => {
|
|
316
|
+
const devflowDir = getDevFlowDirectory();
|
|
317
|
+
const hasFlag = options.status || options.set !== undefined;
|
|
318
|
+
if (!hasFlag) {
|
|
319
|
+
p.intro(color.bgCyan(color.white(' Tracker ')));
|
|
320
|
+
const validIds = TRACKER_PROVIDERS.map(t => `${t.id} — ${t.hint}`).join('\n ');
|
|
321
|
+
p.note(`${color.cyan('devflow tracker --status')} Show the provider and learned conventions\n` +
|
|
322
|
+
`${color.cyan('devflow tracker --set <id>')} Select the issue tracker provider\n\n` +
|
|
323
|
+
`Valid provider IDs:\n ${validIds}`, 'Usage');
|
|
324
|
+
p.outro(color.dim('github is the default — devflow tracker --set github turns the rest off'));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// Validate --set input before any I/O (parse-don't-validate at the boundary).
|
|
328
|
+
let setProvider;
|
|
329
|
+
if (options.set !== undefined) {
|
|
330
|
+
const parsed = parseTrackerId(options.set);
|
|
331
|
+
if (!parsed.ok) {
|
|
332
|
+
p.log.error(parsed.error);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
setProvider = parsed.value;
|
|
336
|
+
}
|
|
337
|
+
const manifest = await readManifest(devflowDir);
|
|
338
|
+
if (!manifest) {
|
|
339
|
+
p.log.error('No manifest found. Run devflow init first.');
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
const current = manifest.features.tracker;
|
|
343
|
+
// ── Status ─────────────────────────────────────────────────────────────────
|
|
344
|
+
// --status wins when both flags are passed, mirroring `devflow compliance`.
|
|
345
|
+
if (options.status) {
|
|
346
|
+
const provenance = await readTrackerProvenance(devflowDir);
|
|
347
|
+
const mechanics = await readTrackerMechanics(getClaudeDirectory(), current.provider);
|
|
348
|
+
// [D-F] Inspecting the status re-arms the attempt counter. --status is
|
|
349
|
+
// the command a capped user reaches for to find out why nothing is being
|
|
350
|
+
// learned, so it is the command that has to hand back another five
|
|
351
|
+
// tries; the alternative leaves the only escape a hand deletion of an
|
|
352
|
+
// undocumented dotfile. Every other --status in this CLI is a pure read,
|
|
353
|
+
// so this one reports the write it makes as a line of the note below — a
|
|
354
|
+
// machine-state change the output does not mention is a change the user
|
|
355
|
+
// cannot audit. Non-fatal exactly as on the --set path (avoids PF-009): a
|
|
356
|
+
// failed re-arm warns, it never aborts the report the user asked for.
|
|
357
|
+
const statusRearm = await rearmTrackerInference(devflowDir);
|
|
358
|
+
const inference = statusRearm.ok
|
|
359
|
+
? `re-armed (${TRACKER_ATTEMPTS_MAX} attempts available)`
|
|
360
|
+
: 're-arm failed — see the warning below';
|
|
361
|
+
const providerLabel = current.provider === DEFAULT_TRACKER_PROVIDER
|
|
362
|
+
? `${color.green(current.provider)} ${color.dim('(default)')}`
|
|
363
|
+
: color.green(current.provider);
|
|
364
|
+
p.note([
|
|
365
|
+
`Provider: ${providerLabel}`,
|
|
366
|
+
`Conventions: ${formatTrackerProvenance(provenance)}`,
|
|
367
|
+
`File: ${trackerConventionsPath(devflowDir)}`,
|
|
368
|
+
`Mechanics: ${formatTrackerMechanics(mechanics)}`,
|
|
369
|
+
`Inference: ${inference}`,
|
|
370
|
+
].join('\n'), 'Tracker Status');
|
|
371
|
+
if (!statusRearm.ok)
|
|
372
|
+
p.log.warn(statusRearm.error);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// ── Set ────────────────────────────────────────────────────────────────────
|
|
376
|
+
const outcome = await runTrackerSet({
|
|
377
|
+
devflowDir,
|
|
378
|
+
claudeDir: getClaudeDirectory(),
|
|
379
|
+
current,
|
|
380
|
+
requested: setProvider ?? current.provider,
|
|
381
|
+
io: buildTrackerSetIO(),
|
|
382
|
+
});
|
|
383
|
+
for (const msg of outcome.messages) {
|
|
384
|
+
switch (msg.level) {
|
|
385
|
+
case 'success':
|
|
386
|
+
p.log.success(msg.text);
|
|
387
|
+
break;
|
|
388
|
+
case 'warn':
|
|
389
|
+
p.log.warn(msg.text);
|
|
390
|
+
break;
|
|
391
|
+
case 'error':
|
|
392
|
+
p.log.error(msg.text);
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
p.log.info(msg.text);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (outcome.exitCode !== 0)
|
|
400
|
+
process.exit(outcome.exitCode);
|
|
401
|
+
if (outcome.provider !== DEFAULT_TRACKER_PROVIDER) {
|
|
402
|
+
p.log.info(color.dim('Your issue conventions are learned in the background at the next session start'));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
//# sourceMappingURL=tracker.js.map
|