devflow-kit 2.5.0 → 3.0.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 +73 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +232 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compliance artifact installer for the Claude Code target.
|
|
3
3
|
*
|
|
4
|
-
* Convergence function: installs
|
|
5
|
-
* and rule file based on the current feature state.
|
|
4
|
+
* Convergence function: installs the compliance skill directory on every
|
|
5
|
+
* machine and installs or removes the rule file based on the current feature state.
|
|
6
|
+
*
|
|
7
|
+
* D-COMPLIANCE-INSTALL-ALWAYS: the skill and all six framework references are
|
|
8
|
+
* installed whatever the machine's own selection, because a repository can turn
|
|
9
|
+
* the review lens on by itself (`compliance` in `.devflow/project.json`, folded
|
|
10
|
+
* into the `COMPLIANCE` field of the settings line). What the machine switch still
|
|
11
|
+
* owns is the RULE — the one artifact Claude Code loads into every prompt — and
|
|
12
|
+
* the stamp on SKILL.md: the machine's frameworks when compliance is on, the
|
|
13
|
+
* neutral zero-framework stamp when it is off. Which references a run loads is
|
|
14
|
+
* decided by the ids its caller passes (D-COMPLIANCE-REPO-LENS), never by which
|
|
15
|
+
* files are present.
|
|
6
16
|
*
|
|
7
17
|
* Applies ADR-013: I/O orchestration in src/targets/; pure helpers in src/core/.
|
|
8
18
|
* Applies PF-009: warn-not-throw for per-item failures.
|
|
@@ -12,10 +22,12 @@
|
|
|
12
22
|
import { promises as fs } from 'fs';
|
|
13
23
|
import * as path from 'path';
|
|
14
24
|
import { skillsDir, rulesDir } from '../../core/assets.js';
|
|
15
|
-
import { ALWAYS_PRESENT_REFS, normalizeFrameworks } from '../../core/compliance.js';
|
|
25
|
+
import { ALWAYS_PRESENT_REFS, COMPLIANCE_FRAMEWORKS, normalizeFrameworks } from '../../core/compliance.js';
|
|
16
26
|
import { parseComplianceFragment, composeComplianceSkill, composeComplianceRule } from '../../core/compliance-compose.js';
|
|
17
27
|
import { validateSkillShadow, validateRuleShadow } from './installer.js';
|
|
18
28
|
// ── Path helpers ───────────────────────────────────────────────────────────
|
|
29
|
+
/** Every registry framework id — the reference set every install carries. */
|
|
30
|
+
const ALL_FRAMEWORK_IDS = COMPLIANCE_FRAMEWORKS.map(fw => fw.id);
|
|
19
31
|
/** Installed compliance skill dir: {claudeDir}/skills/devflow:compliance/ */
|
|
20
32
|
function skillTarget(claudeDir) {
|
|
21
33
|
return path.join(claudeDir, 'skills', 'devflow:compliance');
|
|
@@ -67,13 +79,15 @@ async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
|
|
|
67
79
|
}
|
|
68
80
|
// ── Skill installer ────────────────────────────────────────────────────────
|
|
69
81
|
/**
|
|
70
|
-
* Install the compliance skill directory (
|
|
82
|
+
* Install the compliance skill directory (every reference).
|
|
71
83
|
*
|
|
72
84
|
* SKILL.md source: shadow at {devflowDir}/skills/compliance/SKILL.md (when valid),
|
|
73
|
-
* otherwise canonical src/assets/skills/compliance/SKILL.md.
|
|
85
|
+
* otherwise canonical src/assets/skills/compliance/SKILL.md. It is composed with
|
|
86
|
+
* `stampFrameworks` — the machine's selection, or none for the neutral stamp.
|
|
74
87
|
*
|
|
75
|
-
* Reference files installed: ALWAYS_PRESENT_REFS + one {id}.md
|
|
76
|
-
* References always come from the canonical
|
|
88
|
+
* Reference files installed: ALWAYS_PRESENT_REFS + one {id}.md for EVERY registry
|
|
89
|
+
* framework (D-COMPLIANCE-INSTALL-ALWAYS). References always come from the canonical
|
|
90
|
+
* source — framework refs are not user-overridable.
|
|
77
91
|
*
|
|
78
92
|
* `fragments` is loaded once by convergeComplianceArtifacts and shared with the rule
|
|
79
93
|
* installer — the SKILL.md and the rule compose from the same parsed set.
|
|
@@ -81,7 +95,7 @@ async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
|
|
|
81
95
|
* Applies PF-011: build under a .tmp sibling, remove old target, rename.
|
|
82
96
|
* Applies PF-009: unexpected I/O failures are reported via warn; never thrown.
|
|
83
97
|
*/
|
|
84
|
-
async function installSkillDir(claudeDir, devflowDir,
|
|
98
|
+
async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, warn) {
|
|
85
99
|
const canonicalSrc = path.join(skillsDir(), 'compliance');
|
|
86
100
|
const target = skillTarget(claudeDir);
|
|
87
101
|
const tmpTarget = `${target}.tmp`;
|
|
@@ -100,7 +114,7 @@ async function installSkillDir(claudeDir, devflowDir, frameworks, fragments, war
|
|
|
100
114
|
// SKILL.md: compose from template (shadow or canonical) + fragments.
|
|
101
115
|
// C1: a shadow without tokens passes through byte-identical.
|
|
102
116
|
const templateContent = await fs.readFile(skillMdSrc, 'utf-8');
|
|
103
|
-
const { content: composedSkill, warnings: skillWarnings } = composeComplianceSkill(templateContent,
|
|
117
|
+
const { content: composedSkill, warnings: skillWarnings } = composeComplianceSkill(templateContent, stampFrameworks, fragments);
|
|
104
118
|
for (const w of skillWarnings)
|
|
105
119
|
warn(`compliance: ${w}`);
|
|
106
120
|
await fs.writeFile(path.join(tmpTarget, 'SKILL.md'), composedSkill, 'utf-8');
|
|
@@ -121,9 +135,10 @@ async function installSkillDir(claudeDir, devflowDir, frameworks, fragments, war
|
|
|
121
135
|
}
|
|
122
136
|
// Per-framework reference files: source is frameworks/{id}/reference.md,
|
|
123
137
|
// destination is references/{id}.md (installed artifact layout unchanged — C1
|
|
124
|
-
// for consumers). Every id
|
|
125
|
-
//
|
|
126
|
-
|
|
138
|
+
// for consumers). Every registry id, whatever the machine selected: a repository
|
|
139
|
+
// may declare any of them. The ids come from the static registry — no separator,
|
|
140
|
+
// no traversal. C7: bounded by the registry's six entries.
|
|
141
|
+
for (const fw of ALL_FRAMEWORK_IDS) {
|
|
127
142
|
const srcRef = path.join(canonicalSrc, 'frameworks', fw, 'reference.md');
|
|
128
143
|
const dstRef = path.join(refDst, `${fw}.md`);
|
|
129
144
|
try {
|
|
@@ -183,10 +198,10 @@ async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, war
|
|
|
183
198
|
/**
|
|
184
199
|
* Converge compliance artifacts in the Claude Code install target.
|
|
185
200
|
*
|
|
186
|
-
* Convergence matrix:
|
|
187
|
-
* enabled + rulesEnabled →
|
|
188
|
-
* enabled + !rulesEnabled →
|
|
189
|
-
* !enabled →
|
|
201
|
+
* Convergence matrix (D-COMPLIANCE-INSTALL-ALWAYS):
|
|
202
|
+
* enabled + rulesEnabled → skill dir (every ref, machine stamp) + stamped rule
|
|
203
|
+
* enabled + !rulesEnabled → skill dir (every ref, machine stamp); remove stale rule
|
|
204
|
+
* !enabled → skill dir (every ref, neutral stamp); remove rule
|
|
190
205
|
*
|
|
191
206
|
* PF-015: both artifact operations execute unconditionally — no || short-circuits.
|
|
192
207
|
* PF-011: skill dir write uses temp-sibling+rename to avoid ENOENT windows.
|
|
@@ -218,63 +233,32 @@ export async function convergeComplianceArtifacts(opts) {
|
|
|
218
233
|
converged = false;
|
|
219
234
|
warn(msg);
|
|
220
235
|
};
|
|
221
|
-
// ── Disable path ─────────────────────────────────────────────────────────
|
|
222
|
-
if (!enabled) {
|
|
223
|
-
// Detect pre-existing artifacts BEFORE removal (to set removedPreexisting).
|
|
224
|
-
const skillExisted = await pathExists(skillTarget(claudeDir));
|
|
225
|
-
const ruleExisted = await pathExists(ruleTarget(claudeDir));
|
|
226
|
-
// PF-015: collect results independently — one failure must not skip the other.
|
|
227
|
-
let skillErr = null;
|
|
228
|
-
let ruleErr = null;
|
|
229
|
-
// Step 1: attempt skill dir removal
|
|
230
|
-
try {
|
|
231
|
-
if (skillExisted) {
|
|
232
|
-
await fs.rm(skillTarget(claudeDir), { recursive: true, force: true });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
skillErr = String(err);
|
|
237
|
-
}
|
|
238
|
-
// Step 2: attempt rule removal (runs regardless of Step 1 outcome — PF-015)
|
|
239
|
-
try {
|
|
240
|
-
if (ruleExisted) {
|
|
241
|
-
await fs.rm(ruleTarget(claudeDir), { force: true });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
ruleErr = String(err);
|
|
246
|
-
}
|
|
247
|
-
// PF-009: warn after BOTH attempts so neither failure blocks the other.
|
|
248
|
-
if (skillErr !== null) {
|
|
249
|
-
trackingWarn(`compliance: failed to remove skill dir — ${skillErr}`);
|
|
250
|
-
}
|
|
251
|
-
if (ruleErr !== null) {
|
|
252
|
-
trackingWarn(`compliance: failed to remove rule — ${ruleErr}`);
|
|
253
|
-
}
|
|
254
|
-
return { removedPreexisting: skillExisted || ruleExisted, converged };
|
|
255
|
-
}
|
|
256
|
-
// ── Enable path ──────────────────────────────────────────────────────────
|
|
257
|
-
//
|
|
258
|
-
// PF-015: installSkillDir and the rule step are independent operations.
|
|
259
|
-
// An error in installSkillDir is caught internally and reported via trackingWarn,
|
|
260
|
-
// so execution always continues to the rule step.
|
|
261
236
|
// Fragments are read and parsed once per convergence and shared by both artifacts:
|
|
262
237
|
// they are the same registry-owned files either way, so parsing twice would only
|
|
263
|
-
// duplicate the I/O and report each malformed fragment twice.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
238
|
+
// duplicate the I/O and report each malformed fragment twice. Only the stamped
|
|
239
|
+
// frameworks need one — a compliance-off machine stamps none.
|
|
240
|
+
const stampFrameworks = enabled ? safeFrameworks : [];
|
|
241
|
+
const fragments = await loadComplianceFragments(path.join(skillsDir(), 'compliance'), stampFrameworks, trackingWarn);
|
|
242
|
+
// PF-015: the skill and the rule are independent operations. An error in
|
|
243
|
+
// installSkillDir is caught internally and reported via trackingWarn, so
|
|
244
|
+
// execution always continues to the rule step.
|
|
245
|
+
await installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, trackingWarn);
|
|
246
|
+
if (enabled && rulesEnabled) {
|
|
267
247
|
await installRuleFile(claudeDir, devflowDir, safeFrameworks, fragments, trackingWarn);
|
|
248
|
+
return { removedPreexisting: false, converged };
|
|
268
249
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
250
|
+
// Compliance off, or rules off: no rule. Probe first so a disable convergence
|
|
251
|
+
// can report that it removed one; absence is already the desired end state.
|
|
252
|
+
const ruleExisted = await pathExists(ruleTarget(claudeDir));
|
|
253
|
+
if (ruleExisted) {
|
|
272
254
|
try {
|
|
273
255
|
await fs.rm(ruleTarget(claudeDir), { force: true });
|
|
274
256
|
}
|
|
275
|
-
catch {
|
|
257
|
+
catch (err) {
|
|
258
|
+
trackingWarn(`compliance: failed to remove rule — ${String(err)}`);
|
|
259
|
+
}
|
|
276
260
|
}
|
|
277
|
-
return { removedPreexisting:
|
|
261
|
+
return { removedPreexisting: !enabled && ruleExisted, converged };
|
|
278
262
|
}
|
|
279
263
|
// ── Manifest-slice wrapper ─────────────────────────────────────────────────
|
|
280
264
|
/**
|
|
@@ -1,9 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared hook types for Claude Code settings.json.
|
|
3
|
-
* Used by
|
|
2
|
+
* Shared hook types and hook-ownership helpers for Claude Code settings.json.
|
|
3
|
+
* Used by every module that registers or removes a devflow hook (ambient.ts,
|
|
4
|
+
* capture.ts, memory.ts, context.ts, proxy.ts, legacy-hooks.ts).
|
|
4
5
|
*
|
|
5
6
|
* NOTE: hud.ts uses a structurally different Settings type (statusLine, not hooks)
|
|
6
7
|
* and is intentionally excluded from this shared module.
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
/** The directory, relative to a devflow root, that holds every hook devflow registers. */
|
|
11
|
+
export const HOOKS_DIR_SUFFIX = '/scripts/hooks/';
|
|
12
|
+
/** The command ending devflow writes for a `run-hook <marker>` hook. */
|
|
13
|
+
export function runHookSuffix(marker) {
|
|
14
|
+
return `${HOOKS_DIR_SUFFIX}run-hook ${marker}`;
|
|
15
|
+
}
|
|
16
|
+
/** The command devflow registers for the `run-hook <marker>` hook under `devflowDir`. */
|
|
17
|
+
export function runHookCommand(devflowDir, marker) {
|
|
18
|
+
return `${path.join(devflowDir, 'scripts', 'hooks', 'run-hook')} ${marker}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A predicate matching a hook whose command ends in any of `suffixes`, read with
|
|
22
|
+
* surrounding whitespace trimmed and backslashes as slashes (a Windows install).
|
|
23
|
+
* A missing or non-string command — a hand-edited settings.json — matches nothing.
|
|
24
|
+
*/
|
|
25
|
+
export function endsWithAny(suffixes) {
|
|
26
|
+
return (hook) => {
|
|
27
|
+
const raw = hook.command;
|
|
28
|
+
if (typeof raw !== 'string')
|
|
29
|
+
return false;
|
|
30
|
+
const command = raw.trim().replace(/\\/g, '/');
|
|
31
|
+
return suffixes.some((suffix) => command.endsWith(suffix));
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The ownership predicate for devflow's `run-hook <marker>` hooks.
|
|
36
|
+
*
|
|
37
|
+
* D-EXACT-HOOK-OWNER: a hook is devflow's when its command ENDS in
|
|
38
|
+
* `/scripts/hooks/run-hook <marker>` for one of `markers`, or in one of the
|
|
39
|
+
* module's named `legacySuffixes` (a form an earlier release registered, such as
|
|
40
|
+
* `/scripts/hooks/session-start-memory.sh`), under any directory — so installs made
|
|
41
|
+
* under a custom or retired devflow directory are still recognised. It is never
|
|
42
|
+
* devflow's because it merely CONTAINS a marker word: a user's `~/bin/memory-worker`,
|
|
43
|
+
* `echo capture-turn` or `/opt/tools/run-hook preamble` is theirs (applies ADR-024 —
|
|
44
|
+
* remove only what devflow can prove it wrote). Removal goes through `removeHooks`,
|
|
45
|
+
* one hook at a time. Every hook module builds its predicates here;
|
|
46
|
+
* D-AMBIENT-EXACT-HOOK is the ambient instance of this rule.
|
|
47
|
+
*/
|
|
48
|
+
export function devflowHookOwner(markers, legacySuffixes = []) {
|
|
49
|
+
return endsWithAny([...markers.map(runHookSuffix), ...legacySuffixes]);
|
|
50
|
+
}
|
|
51
|
+
/** A matcher group's hooks, or an empty list for a hand-edited group of another shape. */
|
|
52
|
+
function hooksOf(matcher) {
|
|
53
|
+
return Array.isArray(matcher?.hooks) ? matcher.hooks : [];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Remove every hook matching `shouldRemove` from one event's matcher groups.
|
|
57
|
+
* Mutates `settings` (callers pass their own parsed copy). Returns true if any hook
|
|
58
|
+
* was removed.
|
|
59
|
+
*
|
|
60
|
+
* D-EXACT-HOOK-OWNER: removal is per HOOK, not per matcher group — a group keeps
|
|
61
|
+
* the user's sibling hooks, in their order, and is dropped only when nothing is left
|
|
62
|
+
* in it. A group whose `hooks` is not an array is kept as is. Empty event arrays and
|
|
63
|
+
* an empty `hooks` object are cleaned up.
|
|
64
|
+
*/
|
|
65
|
+
export function removeHooks(settings, eventName, shouldRemove) {
|
|
66
|
+
const matchers = settings.hooks?.[eventName];
|
|
67
|
+
if (!settings.hooks || !Array.isArray(matchers))
|
|
68
|
+
return false;
|
|
69
|
+
let removed = false;
|
|
70
|
+
const kept = [];
|
|
71
|
+
for (const matcher of matchers) {
|
|
72
|
+
const hooks = hooksOf(matcher);
|
|
73
|
+
const remaining = hooks.filter((hook) => !shouldRemove(hook));
|
|
74
|
+
if (remaining.length === hooks.length) {
|
|
75
|
+
kept.push(matcher);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
removed = true;
|
|
79
|
+
if (remaining.length > 0)
|
|
80
|
+
kept.push({ ...matcher, hooks: remaining });
|
|
81
|
+
}
|
|
82
|
+
if (!removed)
|
|
83
|
+
return false;
|
|
84
|
+
if (kept.length === 0) {
|
|
85
|
+
delete settings.hooks[eventName];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
settings.hooks[eventName] = kept;
|
|
89
|
+
}
|
|
90
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
91
|
+
delete settings.hooks;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/** Whether any hook registered for `eventName` matches `isOurs`. */
|
|
96
|
+
export function hasHook(settings, eventName, isOurs) {
|
|
97
|
+
const matchers = settings.hooks?.[eventName];
|
|
98
|
+
return Array.isArray(matchers) && matchers.some((m) => hooksOf(m).some(isOurs));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Append `entry` as a new matcher group for `eventName` unless a hook matching
|
|
102
|
+
* `isOurs` is already registered there. Mutates `settings`. Returns true when the
|
|
103
|
+
* entry was added.
|
|
104
|
+
*/
|
|
105
|
+
export function ensureHook(settings, eventName, isOurs, entry) {
|
|
106
|
+
if (hasHook(settings, eventName, isOurs)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
settings.hooks ??= {};
|
|
110
|
+
settings.hooks[eventName] ??= [];
|
|
111
|
+
settings.hooks[eventName].push(entry);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
9
114
|
//# sourceMappingURL=hooks.js.map
|
|
@@ -215,11 +215,10 @@ export async function chmodRecursive(dir, mode, _depth = 0) {
|
|
|
215
215
|
* `tracker/` ({@link TRACKER_DESTINATION_ROOT}) holds the per-provider mechanics and `pr/`
|
|
216
216
|
* ({@link PR_HOST_DESTINATION_ROOT}) the PR/review host bodies; the build emits both
|
|
217
217
|
* wholesale, so anything inside them the manifest does not name is by construction a
|
|
218
|
-
* leftover — a retired op, a provider the
|
|
218
|
+
* leftover — a retired op, a provider the registry dropped, a shadow-supplied file, a
|
|
219
219
|
* staging tree a crashed run stranded — and removing it is the only way the installed
|
|
220
|
-
* tree can equal the manifest.
|
|
221
|
-
*
|
|
222
|
-
* directory's CONTENTS, exactly as `tracker/`'s are converged. Both entries are the
|
|
220
|
+
* tree can equal the manifest. Every install carries both whole
|
|
221
|
+
* (D-INSTALL-ALL-PROVIDERS), so what the prune converges is each directory's CONTENTS. Both entries are the
|
|
223
222
|
* registry's own constants, so a renamed destination root moves the build and the prune
|
|
224
223
|
* together.
|
|
225
224
|
*
|
|
@@ -1067,42 +1066,6 @@ export async function overlayGeneratedReferences(opts) {
|
|
|
1067
1066
|
}
|
|
1068
1067
|
return { overlaidRefs, unchangedRefs, overlayFailures, pruned };
|
|
1069
1068
|
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Converge the installed `devflow:git` references onto ONE provider's install set.
|
|
1072
|
-
*
|
|
1073
|
-
* The provider-scoped entry point to {@link overlayGeneratedReferences}: it
|
|
1074
|
-
* resolves the install manifest and the target directory from a claudeDir and a
|
|
1075
|
-
* provider, and changes nothing else. There is exactly ONE overlay spelling in
|
|
1076
|
-
* this codebase and this is its only wrapper — `devflow init` reaches the
|
|
1077
|
-
* overlay through `installViaFileCopy`, `devflow tracker --set` reaches it
|
|
1078
|
-
* through here, and both converge to the same manifest for the same provider.
|
|
1079
|
-
*
|
|
1080
|
-
* Convergence is two-directional by construction, because the underlying overlay
|
|
1081
|
-
* PRUNES everything under its converged subtrees the manifest does not name: a
|
|
1082
|
-
* jira → github change removes the jira tree and `_mcp.md` in the same call that
|
|
1083
|
-
* refreshes the github tree (applies PF-015). `references/pr/` is wanted under
|
|
1084
|
-
* every provider (applies ADR-026), so a provider change leaves it standing —
|
|
1085
|
-
* converged, not removed.
|
|
1086
|
-
*
|
|
1087
|
-
* Throws on an absent generated tree, exactly as its callee does — that is a
|
|
1088
|
-
* build artifact that was never produced, not an I/O degradation, and the
|
|
1089
|
-
* refusal lands before the target directory is created so a refused overlay
|
|
1090
|
-
* leaves the install as it found it.
|
|
1091
|
-
*
|
|
1092
|
-
* @param opts.provider - The RESOLVED tracker provider id.
|
|
1093
|
-
* @param opts.referencesRoot - The GENERATED tree to install from; defaults to
|
|
1094
|
-
* `compiledSkillRefsDir()`. Injectable so the absent-tree refusal is provable
|
|
1095
|
-
* without deleting `dist/` out from under a concurrent test run (applies
|
|
1096
|
-
* PF-013 — a seam the caller can drive, not a global the test has to break).
|
|
1097
|
-
*/
|
|
1098
|
-
export async function overlayInstalledReferences(opts) {
|
|
1099
|
-
return overlayGeneratedReferences({
|
|
1100
|
-
referencesTarget: path.join(opts.claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'references'),
|
|
1101
|
-
sourceRoot: opts.referencesRoot,
|
|
1102
|
-
manifest: installedReferenceManifest({ provider: opts.provider }),
|
|
1103
|
-
warn: opts.warn,
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
1069
|
/** The directory inside an installed skill that the reference overlay converges. */
|
|
1107
1070
|
const SKILL_REFERENCES_DIRNAME = 'references';
|
|
1108
1071
|
/**
|
|
@@ -1229,11 +1192,24 @@ function collectRelativeImports(source) {
|
|
|
1229
1192
|
export async function composeScripts(scriptsTarget) {
|
|
1230
1193
|
await fs.mkdir(scriptsTarget, { recursive: true });
|
|
1231
1194
|
// (a) src/assets/scripts/ verbatim
|
|
1195
|
+
//
|
|
1196
|
+
// D-SCRIPTS-EXEC-SCOPE: only what this step copied is made executable — each of the
|
|
1197
|
+
// source tree's top-level entries, at its destination. A chmod over the whole target
|
|
1198
|
+
// would also reach what (b) and (c) wrote there on an earlier run (package.json, the
|
|
1199
|
+
// mirrored dist/hud/ closure), so a re-run gave package.json an exec bit the first
|
|
1200
|
+
// run never did and re-init stopped being a no-op on disk (#388 AC-3). Scoping the
|
|
1201
|
+
// chmod to the copied entries makes the first and every later run agree.
|
|
1232
1202
|
const srcScripts = scriptsDir();
|
|
1233
1203
|
try {
|
|
1234
1204
|
await copyDirectory(srcScripts, scriptsTarget);
|
|
1235
1205
|
if (process.platform !== 'win32') {
|
|
1236
|
-
await
|
|
1206
|
+
for (const entry of await fs.readdir(srcScripts, { withFileTypes: true })) {
|
|
1207
|
+
const copied = path.join(scriptsTarget, entry.name);
|
|
1208
|
+
if (entry.isDirectory())
|
|
1209
|
+
await chmodRecursive(copied, 0o755);
|
|
1210
|
+
else if (entry.isFile())
|
|
1211
|
+
await fs.chmod(copied, 0o755);
|
|
1212
|
+
}
|
|
1237
1213
|
}
|
|
1238
1214
|
}
|
|
1239
1215
|
catch { /* scripts dir may not exist yet during development */ }
|
|
@@ -1392,11 +1368,11 @@ export async function installViaFileCopy(options) {
|
|
|
1392
1368
|
// needed (D-OVERLAY-OWNERSHIP), for the same reason. The agent directory is
|
|
1393
1369
|
// emptied AROUND the one file `convergeTrackerArtifacts` owns: taking it
|
|
1394
1370
|
// would leave converge with nothing to byte-compare against, so a
|
|
1395
|
-
// steady-state
|
|
1371
|
+
// steady-state re-init would re-copy the agent and announce
|
|
1396
1372
|
// `tracker agent installed` on every run. Everything else is removed
|
|
1397
1373
|
// exactly as the unconditional wipe removed it, and the file is still
|
|
1398
|
-
// converged on this run —
|
|
1399
|
-
//
|
|
1374
|
+
// converged on this run — drift in it is restored, so preserving it strands
|
|
1375
|
+
// nothing.
|
|
1400
1376
|
try {
|
|
1401
1377
|
await emptyDirectoryExcept(path.join(claudeDir, 'agents', 'devflow'), new Set([mdFileName(TRACKER_AGENT_NAME)]));
|
|
1402
1378
|
}
|
|
@@ -1451,7 +1427,7 @@ export async function installViaFileCopy(options) {
|
|
|
1451
1427
|
// file — including a reference at the references ROOT, which the overlay may replace
|
|
1452
1428
|
// but never delete (D-OVERLAY-FLAT-UNIT) — does not survive a full install.
|
|
1453
1429
|
if (!isPartialInstall) {
|
|
1454
|
-
const overlayOwned = overlayOwnedSkillPaths(installedReferenceManifest(
|
|
1430
|
+
const overlayOwned = overlayOwnedSkillPaths(installedReferenceManifest());
|
|
1455
1431
|
for (const skill of skillsMap.keys()) {
|
|
1456
1432
|
// Empty the prefixed directory (its contents are re-created during the install
|
|
1457
1433
|
// phase), minus whatever another converger owns inside it.
|
|
@@ -1517,14 +1493,12 @@ export async function installViaFileCopy(options) {
|
|
|
1517
1493
|
// build/packaging failure and throws rather than silently skipping (matches
|
|
1518
1494
|
// command pattern); the message names the build step as well as the tree.
|
|
1519
1495
|
//
|
|
1520
|
-
// D-TRACKER-AGENT-OWNER: every declared agent but ONE. The Tracker agent
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
// same run had written, and a fresh jira install never reported `installed`
|
|
1527
|
-
// because converge found this loop's byte-identical copy already in place.
|
|
1496
|
+
// D-TRACKER-AGENT-OWNER: every declared agent but ONE. The Tracker agent is
|
|
1497
|
+
// converged by `convergeTrackerArtifacts` alone (plan A3), which runs after this
|
|
1498
|
+
// function in init and reports whether this run wrote it. Copying it here too
|
|
1499
|
+
// would make every install do the work twice, and a fresh install would never
|
|
1500
|
+
// report `installed` because converge would find this loop's byte-identical copy
|
|
1501
|
+
// already in place.
|
|
1528
1502
|
//
|
|
1529
1503
|
// The name is skipped from the COPY set only. It stays declared in
|
|
1530
1504
|
// `devflow-core-skills.agents`, so the sweep below — which keys on the full
|
|
@@ -1598,10 +1572,9 @@ export async function installViaFileCopy(options) {
|
|
|
1598
1572
|
if (skillName === SKILL_REFS_SKILL_NAME) {
|
|
1599
1573
|
const overlay = await overlayGeneratedReferences({
|
|
1600
1574
|
referencesTarget: path.join(skillTarget, 'references'),
|
|
1601
|
-
//
|
|
1602
|
-
//
|
|
1603
|
-
|
|
1604
|
-
manifest: installedReferenceManifest({ provider: options.trackerProvider }),
|
|
1575
|
+
// Every provider's mechanics (D-INSTALL-ALL-PROVIDERS). The overlay
|
|
1576
|
+
// converges rather than merges, so a retired generated document is pruned.
|
|
1577
|
+
manifest: installedReferenceManifest(),
|
|
1605
1578
|
warn,
|
|
1606
1579
|
});
|
|
1607
1580
|
report.overlaidRefs.push(...overlay.overlaidRefs);
|