devflow-kit 3.0.1 → 3.2.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 +49 -0
- package/README.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +71 -17
- package/dist/cli/agents-view/state.js +42 -16
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +142 -51
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +162 -85
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +302 -136
- package/dist/cli/commands/memory.js +36 -15
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +183 -59
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +51 -47
- package/dist/commands/dynamic-plan.md +27 -7
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/explore.md +9 -3
- package/dist/commands/implement.md +20 -16
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +23 -3
- package/dist/commands/research.md +9 -3
- package/dist/commands/resolve.md +9 -12
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +28 -3
- package/dist/core/agent-models.js +204 -42
- package/dist/core/agent-state.js +28 -6
- package/dist/core/ansi.js +2 -2
- package/dist/core/assets.js +1 -1
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +71 -13
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +49 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -81
- package/dist/core/learning-store.js +61 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +13 -8
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/queue-drain.js +31 -0
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +54 -22
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/targets/claude-code/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +59 -32
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +135 -45
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +15 -21
- package/src/assets/agents/design.md +4 -2
- package/src/assets/agents/diagnose.md +3 -1
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +5 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +3 -1
- package/src/assets/agents/review.md +5 -3
- package/src/assets/agents/scrutinize.md +5 -1
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +4 -2
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +11 -9
- package/src/assets/agents/validate.md +14 -10
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +16 -32
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/code-review.mds +0 -2
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +18 -12
- package/src/assets/commands/dynamic-plan.mds +10 -4
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +19 -13
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +23 -3
- package/src/assets/commands/research.mds +9 -3
- package/src/assets/commands/resolve.mds +9 -10
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +3 -3
- package/src/assets/mds/tracker/_jira.mds +3 -3
- package/src/assets/mds/tracker/_linear.mds +3 -3
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
- package/src/assets/scripts/hooks/background-memory-update +97 -33
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +5 -20
- package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +345 -944
- package/src/assets/scripts/hooks/json-parse +25 -129
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +55 -23
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +146 -45
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/src/assets/skills/test-driven-development/SKILL.md +6 -4
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -77,9 +77,9 @@ export async function validateRuleShadow(shadowFile) {
|
|
|
77
77
|
*
|
|
78
78
|
* D: Missing declared source is a build/packaging failure — throws rather than
|
|
79
79
|
* silently returning 'skipped' (mirrors command hard-error pattern). Per-item
|
|
80
|
-
* copy failures (EACCES, ENOSPC, etc.) are still isolated
|
|
81
|
-
*
|
|
82
|
-
* Invalid shadows still warn-and-install-source
|
|
80
|
+
* copy failures (EACCES, ENOSPC, etc.) are still isolated
|
|
81
|
+
* (one bad copy does not abort the whole batch).
|
|
82
|
+
* Invalid shadows still warn-and-install-source.
|
|
83
83
|
*/
|
|
84
84
|
export async function installRuleFile(ruleName, devflowDir, rulesTarget) {
|
|
85
85
|
const shadowFile = path.join(devflowDir, 'rules', mdFileName(ruleName));
|
|
@@ -114,7 +114,7 @@ export async function installRuleFile(ruleName, devflowDir, rulesTarget) {
|
|
|
114
114
|
throw new Error(`Rule source not found for declared rule "${ruleName}": ${ruleSource}. ` +
|
|
115
115
|
`Ensure the rule file exists in src/assets/rules/.`);
|
|
116
116
|
}
|
|
117
|
-
// Copy is isolated
|
|
117
|
+
// Copy is isolated: a copy failure degrades to 'skipped' so one
|
|
118
118
|
// bad rule does not abort the entire installAllRules Promise.all batch.
|
|
119
119
|
try {
|
|
120
120
|
await fs.copyFile(ruleSource, targetFile);
|
|
@@ -236,8 +236,8 @@ export async function chmodRecursive(dir, mode, _depth = 0) {
|
|
|
236
236
|
* converges to empty. The list is also the one thing that lets the pre-clean keep a
|
|
237
237
|
* nested directory whole ({@link overlayOwnedSkillPaths}), so a directory missing from
|
|
238
238
|
* it fails safe — kept file by file, like the root — rather than keeping its stale files
|
|
239
|
-
* forever
|
|
240
|
-
* that entry is the whole edit: everything downstream reads the list
|
|
239
|
+
* forever. The cost is one entry per new wholly-generated directory, and
|
|
240
|
+
* that entry is the whole edit: everything downstream reads the list.
|
|
241
241
|
*/
|
|
242
242
|
const CONVERGED_SUBTREES = [TRACKER_DESTINATION_ROOT, PR_HOST_DESTINATION_ROOT];
|
|
243
243
|
/** Is this top-level directory under the references root one the prune converges? */
|
|
@@ -248,7 +248,7 @@ function isConvergedSubtree(top) {
|
|
|
248
248
|
* One spelling of a unit's name, for every message about it.
|
|
249
249
|
*
|
|
250
250
|
* Pure function — the installer owns the unit types, so it owns how they are named,
|
|
251
|
-
* rather than leaving each render site to invent its own wording
|
|
251
|
+
* rather than leaving each render site to invent its own wording.
|
|
252
252
|
*/
|
|
253
253
|
export function overlayUnitLabel(unit) {
|
|
254
254
|
switch (unit.kind) {
|
|
@@ -317,7 +317,7 @@ function isProviderSubdir(subdir) {
|
|
|
317
317
|
* tracker/{provider}`, so a `tracker/` entry mis-bucketed as a provider renames the
|
|
318
318
|
* whole subtree into place BEFORE the provider units promote back into it, and the
|
|
319
319
|
* installed tree ends up complete under either rule. The classification itself is the
|
|
320
|
-
* observation that separates them
|
|
320
|
+
* observation that separates them.
|
|
321
321
|
*/
|
|
322
322
|
export function planOverlayUnits(manifest) {
|
|
323
323
|
const bySubdir = new Map();
|
|
@@ -486,8 +486,8 @@ function subtreesTouchedBy(referencesTarget, unit) {
|
|
|
486
486
|
* degradation, and shipping an installer that silently omits the mechanics the agent is
|
|
487
487
|
* told to load would move the failure to every user's first spawn.
|
|
488
488
|
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
489
|
+
* Builds under a `.tmp` sibling, pre-cleaning an orphan from a prior crashed run.
|
|
490
|
+
* Every other failure is isolated to its unit: a copy that fails aborts this unit
|
|
491
491
|
* and no other.
|
|
492
492
|
*/
|
|
493
493
|
async function buildUnitStagingTree(unit, sourceRoot, referencesTarget, warn) {
|
|
@@ -685,7 +685,7 @@ async function promoteDirectoryUnit(unit, referencesTarget, stagingDir, record)
|
|
|
685
685
|
// while the report — and the summary line init.ts renders from it — still claims
|
|
686
686
|
// the previously installed files were left unchanged. The backup is what makes
|
|
687
687
|
// that claim true, so a failed promotion is recoverable rather than a silent
|
|
688
|
-
// deletion (
|
|
688
|
+
// deletion (a reported failure must describe the state it left).
|
|
689
689
|
//
|
|
690
690
|
// The `.old` backup is pre-cleaned like the `.tmp` tree. A crash that strands
|
|
691
691
|
// either is converged away by a later run's tracker-subtree prune (both names end
|
|
@@ -804,7 +804,7 @@ async function classifyUntouchedUnit(unit, referencesTarget) {
|
|
|
804
804
|
* not cover — and the same root cause the agent resolver in `installViaFileCopy` already
|
|
805
805
|
* throws for, so the two build artifacts are guarded at the same strength.
|
|
806
806
|
*
|
|
807
|
-
* Deliberately ONE `stat` before the unit loop rather than a check inside it
|
|
807
|
+
* Deliberately ONE `stat` before the unit loop rather than a check inside it:
|
|
808
808
|
* the fan-out has no per-item failure isolation, so a per-unit refusal would let one
|
|
809
809
|
* unbuilt provider abort every other unit's install. A unit directory that is absent
|
|
810
810
|
* under a root that exists stays a per-unit report, exactly as today.
|
|
@@ -852,7 +852,7 @@ async function requireGeneratedTree(sourceRoot, manifest) {
|
|
|
852
852
|
*
|
|
853
853
|
* The skip is not silent. The unswept subtree is reported through `failed` — the same
|
|
854
854
|
* channel that module uses for its own depth-bound breach — so nothing claims
|
|
855
|
-
* convergence over ground it did not cover
|
|
855
|
+
* convergence over ground it did not cover. Orphans under that
|
|
856
856
|
* subtree survive this install and the next one converges them.
|
|
857
857
|
*
|
|
858
858
|
* A subtree whose root is not a real directory ({@link convergedRootFault}) is skipped
|
|
@@ -974,8 +974,8 @@ async function pruneConvergedSubtrees(referencesTarget, manifest, overlayFailure
|
|
|
974
974
|
* @param opts.warn - Receives non-fatal notices (skipped symlinks, mode normalisation).
|
|
975
975
|
*
|
|
976
976
|
* @throws on three conditions, each of them a build artifact that was never produced
|
|
977
|
-
* rather than an I/O degradation. Every other failure is reported, never thrown
|
|
978
|
-
*
|
|
977
|
+
* rather than an I/O degradation. Every other failure is reported, never thrown,
|
|
978
|
+
* and the three are ordered here as the function reaches them:
|
|
979
979
|
* 1. `opts.manifest` omitted AND the reference-module registry does not expand —
|
|
980
980
|
* raised by {@link generatedReferenceManifest} while resolving the default. A
|
|
981
981
|
* caller that passes its own manifest cannot reach this one.
|
|
@@ -1048,12 +1048,12 @@ export async function overlayGeneratedReferences(opts) {
|
|
|
1048
1048
|
// this run installed. copyDirectory preserves source modes, so a hand-authored
|
|
1049
1049
|
// reference checked in with an odd mode installs with it; a reference is read-only
|
|
1050
1050
|
// instruction text and 0644 is what every one of them should be. Best-effort: a
|
|
1051
|
-
// filesystem that does not honour mode bits must not fail an install
|
|
1051
|
+
// filesystem that does not honour mode bits must not fail an install.
|
|
1052
1052
|
//
|
|
1053
1053
|
// This is the one step that reaches a file the overlay does not own, and it is why the
|
|
1054
1054
|
// boundary is stated as "never replace or delete" rather than "never touch": the MODE of
|
|
1055
1055
|
// a hand-authored reference — and of whatever a shadowed skill supplied outside
|
|
1056
|
-
// `tracker/` — is normalised here.
|
|
1056
|
+
// `tracker/` — is normalised here. The prove-you-wrote-it rule permits exactly that: the
|
|
1057
1057
|
// ownership guard protects deletion, not overwrite.
|
|
1058
1058
|
//
|
|
1059
1059
|
// It is also the one walk that can breach chmodRecursive's descent bound. The catch is
|
|
@@ -1096,9 +1096,9 @@ const SKILL_REFERENCES_DIRNAME = 'references';
|
|
|
1096
1096
|
* Deciding the subtree arm by {@link CONVERGED_SUBTREES} rather than by nesting is what
|
|
1097
1097
|
* makes the two lists agree by construction: a directory is kept whole exactly when a
|
|
1098
1098
|
* prune owns it, so a fan-out directory registered without a converged entry fails safe
|
|
1099
|
-
* instead of surviving every install
|
|
1099
|
+
* instead of surviving every install.
|
|
1100
1100
|
*
|
|
1101
|
-
* Pure function
|
|
1101
|
+
* Pure function. Exported for the one property no installed-tree arm
|
|
1102
1102
|
* can reach: the build emits no nested directory outside the converged list, so the
|
|
1103
1103
|
* fail-safe arm is only observable on a manifest the registry does not produce.
|
|
1104
1104
|
*/
|
|
@@ -1284,7 +1284,7 @@ async function firstExisting(candidates) {
|
|
|
1284
1284
|
* One readdir of the SHADOW tree, intersected with the registry. Deliberately
|
|
1285
1285
|
* not a readdir of the installed skills directory: that tree is the thing being
|
|
1286
1286
|
* converged, and reading it to decide what to remove is how a directory a user
|
|
1287
|
-
* put there by hand becomes a deselection
|
|
1287
|
+
* put there by hand becomes a deselection.
|
|
1288
1288
|
*
|
|
1289
1289
|
* Whether a shadow is VALID is a separate question, answered per skill by
|
|
1290
1290
|
* validateSkillShadow at install time. This only answers "did the user write
|
|
@@ -1310,6 +1310,35 @@ function recordSweep(report, kind, sweep) {
|
|
|
1310
1310
|
report.sweptOrphans.push(...sweep.removed.map(name => ({ kind, name })));
|
|
1311
1311
|
report.sweepFailures.push(...sweep.failed.map(f => ({ kind, name: f.name, error: f.error })));
|
|
1312
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Remove the named `devflow:`-prefixed skill directories from `skillsDir` and
|
|
1315
|
+
* report which of them were actually there to remove.
|
|
1316
|
+
*
|
|
1317
|
+
* `names` is registry arithmetic (the skills no selected plugin owns), so most of
|
|
1318
|
+
* it is routinely absent from disk. Only the deleter can say what it deleted:
|
|
1319
|
+
* `fs.rm` is called WITHOUT `force`, so an absent directory rejects with ENOENT —
|
|
1320
|
+
* "nothing to remove", neither a removal nor a failure. Reporting the plan instead
|
|
1321
|
+
* would tell the user about deletions that never happened.
|
|
1322
|
+
*
|
|
1323
|
+
* Per-item failure isolation, never throws: any other rejection is recorded in
|
|
1324
|
+
* `failed` and the remaining names are still attempted.
|
|
1325
|
+
*/
|
|
1326
|
+
async function removeDeselectedSkills(skillsDir, names) {
|
|
1327
|
+
const removed = [];
|
|
1328
|
+
const failed = [];
|
|
1329
|
+
for (const name of names) {
|
|
1330
|
+
try {
|
|
1331
|
+
await fs.rm(path.join(skillsDir, prefixSkillName(name)), { recursive: true });
|
|
1332
|
+
removed.push(name);
|
|
1333
|
+
}
|
|
1334
|
+
catch (err) {
|
|
1335
|
+
if (err.code === 'ENOENT')
|
|
1336
|
+
continue;
|
|
1337
|
+
failed.push({ name, error: err });
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return { removed, failed };
|
|
1341
|
+
}
|
|
1313
1342
|
/**
|
|
1314
1343
|
* Install plugins via manual file copy.
|
|
1315
1344
|
* Handles cleanup of old monolithic structure, deduplication of shared assets,
|
|
@@ -1336,7 +1365,7 @@ export async function installViaFileCopy(options) {
|
|
|
1336
1365
|
// Pure and registry-driven: the removal set is `skillsOf(all) \ skillsOf(selected)
|
|
1337
1366
|
// \ FEATURE_OWNED`, never a readdir of the installed skills directory, so an
|
|
1338
1367
|
// unrelated `devflow:` directory a user put there by hand is not swept as a
|
|
1339
|
-
// deselection
|
|
1368
|
+
// deselection.
|
|
1340
1369
|
//
|
|
1341
1370
|
// Computed BEFORE shadows are resolved: a shadow is applied only to a skill the
|
|
1342
1371
|
// selection installs, so the install set is the question that has to be settled
|
|
@@ -1383,7 +1412,7 @@ export async function installViaFileCopy(options) {
|
|
|
1383
1412
|
// knownNames spans ALL plugins (getAllSkillNames) so skills from uninstalled
|
|
1384
1413
|
// plugins survive a partial run. Bare (pre-namespace) dirs are intentionally
|
|
1385
1414
|
// untouched — they are handled by the frozen LEGACY_SKILLS_* lists in legacy.ts
|
|
1386
|
-
// (
|
|
1415
|
+
// (those lists are deletion manifests for pre-namespace paths and
|
|
1387
1416
|
// must not be modified). Shadow dirs (~/.devflow/skills/) are keyed by bare
|
|
1388
1417
|
// registry name and are unaffected by this sweep.
|
|
1389
1418
|
// knownNames unions FEATURE_OWNED_SKILLS so feature-owned skills (e.g. devflow:compliance)
|
|
@@ -1396,7 +1425,7 @@ export async function installViaFileCopy(options) {
|
|
|
1396
1425
|
// ~/.claude/skills/{name} are owned solely by the frozen LEGACY_SKILL_NAMES
|
|
1397
1426
|
// pass in init.ts (runs immediately after this call). A bare dir whose name
|
|
1398
1427
|
// matches a current registry skill is by construction foreign to Devflow and
|
|
1399
|
-
// must not be touched here
|
|
1428
|
+
// must not be touched here.
|
|
1400
1429
|
//
|
|
1401
1430
|
// The pre-clean is SCOPED to what this run reinstalls and the orphan sweep
|
|
1402
1431
|
// above is UNSCOPED (the full registry). The opposite scoping is deliberate,
|
|
@@ -1446,15 +1475,13 @@ export async function installViaFileCopy(options) {
|
|
|
1446
1475
|
// Remove the skills no selected plugin owns or requires — the deselection half
|
|
1447
1476
|
// of the scoped install. Empty on a partial install by construction
|
|
1448
1477
|
// (resolveSkillInstallPlan gates it), so `--plugin=X` adds and never subtracts
|
|
1449
|
-
// (AC-22). Failures are per-item and non-fatal
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
warn(`Could not remove deselected skill "${prefixSkillName(skill)}" — ${String(err)}`);
|
|
1457
|
-
}
|
|
1478
|
+
// (AC-22). Failures are per-item and non-fatal. `removedSkills` lists only what
|
|
1479
|
+
// was on disk and is now gone: the plan is registry arithmetic and mostly names
|
|
1480
|
+
// skills that were never installed.
|
|
1481
|
+
const deselected = await removeDeselectedSkills(path.join(claudeDir, 'skills'), skillPlan.remove);
|
|
1482
|
+
report.removedSkills.push(...deselected.removed);
|
|
1483
|
+
for (const failure of deselected.failed) {
|
|
1484
|
+
warn(`Could not remove deselected skill "${prefixSkillName(failure.name)}" — ${String(failure.error)}`);
|
|
1458
1485
|
}
|
|
1459
1486
|
// Install commands from selected plugins using registry-driven lookup.
|
|
1460
1487
|
// Source: dist/commands/{name}.md (single lookup directory for all commands).
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* These lists are FROZEN deletion manifests for names that once shipped: the
|
|
12
12
|
* `*_V2` suffix is the frozen spelling of an era, not a version to bump, and a
|
|
13
13
|
* name is removed from a list only when its pruning window has passed — never
|
|
14
|
-
* renamed to match a current registry name
|
|
14
|
+
* renamed to match a current registry name.
|
|
15
15
|
*/
|
|
16
16
|
/** Pre-v1.0.0: devflow- prefixed skill names from the original install scheme. */
|
|
17
17
|
const LEGACY_SKILLS_PRE_V1 = [
|
|
@@ -4,6 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import { getManagedSettingsPath } from './claude-paths.js';
|
|
6
6
|
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
7
|
+
import { firstSymbolicLink } from '../../core/linked-path.js';
|
|
7
8
|
function isNodeSystemError(error) {
|
|
8
9
|
return (error instanceof Error &&
|
|
9
10
|
'code' in error &&
|
|
@@ -37,7 +38,7 @@ const CLAUDEIGNORE_NEGATION = '!.claudeignore';
|
|
|
37
38
|
/**
|
|
38
39
|
* Re-includes the retired evidence-policy file (D-GITIGNORE-V5,
|
|
39
40
|
* D-POLICY-JSON-RETIRED). A COMPLETION line, never a presence sentinel: users may author it themselves, so its presence
|
|
40
|
-
* proves nothing about the devflow block
|
|
41
|
+
* proves nothing about the devflow block. It sits after `.devflow/*`
|
|
41
42
|
* (which it overrides under last-match-wins) and before `.claudeignore`, so the
|
|
42
43
|
* block's final line stays `.claudeignore`.
|
|
43
44
|
*/
|
|
@@ -46,11 +47,11 @@ const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
|
|
|
46
47
|
* Re-includes the team-committed project settings file (D-GITIGNORE-V6). The same
|
|
47
48
|
* contract as the policy line: a COMPLETION line, never a presence sentinel — a
|
|
48
49
|
* user may author it before devflow ever runs, so its presence proves nothing about
|
|
49
|
-
* the block
|
|
50
|
+
* the block. It sits after the policy line and before `.claudeignore`,
|
|
50
51
|
* so a v5 block, which ends in `.claudeignore`, gains it just before that line
|
|
51
52
|
* (D-GITIGNORE-IN-BLOCK, computeDevflowGitignore). Without it
|
|
52
53
|
* `.devflow/*` ignores `.devflow/project.json`, and a team could only commit it with
|
|
53
|
-
* `git add -f`. Devflow never writes the file itself
|
|
54
|
+
* `git add -f`. Devflow never writes the file itself.
|
|
54
55
|
*/
|
|
55
56
|
const DEVFLOW_PROJECT_LINE = '!.devflow/project.json';
|
|
56
57
|
/**
|
|
@@ -143,7 +144,7 @@ const BLOCK_RUN_MAX = 3;
|
|
|
143
144
|
* user-authored line inverts both halves of the contract: projects that already carry
|
|
144
145
|
* that line are told the block is installed when it is not, and a user's
|
|
145
146
|
* `!.claudeignore` un-ignore is silently reversed by re-appending `.claudeignore`
|
|
146
|
-
* under last-match-wins
|
|
147
|
+
* under last-match-wins.
|
|
147
148
|
*
|
|
148
149
|
* `hasClaudeignoreEntry` is true when some whole line, trimmed, is exactly
|
|
149
150
|
* `.claudeignore` OR `!.claudeignore`. Treating both forms as "present" both honours
|
|
@@ -301,7 +302,7 @@ export function mergeDenyList(existingJson, newDenyEntries, retired = new Set())
|
|
|
301
302
|
// holds it. (Claude Code's own suggestion, `-v /*`, would deny every absolute mount.)
|
|
302
303
|
// Only entries a release actually shipped belong here: removal and install convergence
|
|
303
304
|
// strip every entry this set names that the template does not, so a rule Devflow never
|
|
304
|
-
// shipped would be taken from a user who wrote it (
|
|
305
|
+
// shipped would be taken from a user who wrote it (prove-you-wrote-it).
|
|
305
306
|
export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
|
|
306
307
|
// v1 batch — 154 entries shipped in src/targets/claude-code/templates/managed-settings.json
|
|
307
308
|
'Bash(rm -rf /*)',
|
|
@@ -500,7 +501,7 @@ export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
|
|
|
500
501
|
* An empty template (loadTemplateDenyEntries' failure value) retires nothing — an
|
|
501
502
|
* unreadable template must never read as "Devflow dropped every entry it ever shipped".
|
|
502
503
|
*
|
|
503
|
-
* Accepted trade-off
|
|
504
|
+
* Accepted trade-off: a deny entry is a bare string, so a user who typed a
|
|
504
505
|
* retired entry themselves is indistinguishable from Devflow's copy and loses it on the
|
|
505
506
|
* next install, exactly as `security --disable` and uninstall already strip every
|
|
506
507
|
* historical entry. Retire an entry only when losing a user's identical copy is
|
|
@@ -981,7 +982,7 @@ function hookCommandsOf(matcher) {
|
|
|
981
982
|
*
|
|
982
983
|
* `existing` comes from a hand-editable file, so every branch is shape-guarded:
|
|
983
984
|
* a `hooks` value (or per-event value) that is not the expected object/array shape
|
|
984
|
-
* is left untouched rather than overwritten or thrown on (
|
|
985
|
+
* is left untouched rather than overwritten or thrown on (validate
|
|
985
986
|
* at the sink that mutates).
|
|
986
987
|
*
|
|
987
988
|
* Exported for testing.
|
|
@@ -1124,6 +1125,20 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
|
|
|
1124
1125
|
return false;
|
|
1125
1126
|
}
|
|
1126
1127
|
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Whether `gitRoot` already holds a `.claudeignore`, as
|
|
1130
|
+
* {@link installClaudeignore}'s exclusive create sees it: lstat, not stat, so a
|
|
1131
|
+
* dangling symlink counts as present — the create refuses one too.
|
|
1132
|
+
*/
|
|
1133
|
+
export async function hasClaudeignore(gitRoot) {
|
|
1134
|
+
try {
|
|
1135
|
+
await fs.lstat(path.join(gitRoot, '.claudeignore'));
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1127
1142
|
/**
|
|
1128
1143
|
* Discover git repository roots from Claude's project history.
|
|
1129
1144
|
* Parses `<claudeDir>/history.jsonl` for unique project paths that are valid git repos.
|
|
@@ -1175,9 +1190,9 @@ export async function discoverProjectGitRoots(claudeDir) {
|
|
|
1175
1190
|
*/
|
|
1176
1191
|
const GITIGNORE_MARKER_V6 = '.root-gitignore-configured-v6';
|
|
1177
1192
|
/**
|
|
1178
|
-
* Earlier markers, the unversioned (v1) one included — every
|
|
1179
|
-
*
|
|
1180
|
-
* re-stamp one beside v6, and the shell twin drops the same five.
|
|
1193
|
+
* Earlier markers, the unversioned (v1) one included — every run removes all of them
|
|
1194
|
+
* once v6 is stamped, a project already stamped included: an older devflow can
|
|
1195
|
+
* re-stamp one beside v6, and the shell twin drops the same five, on its fast path too.
|
|
1181
1196
|
*/
|
|
1182
1197
|
const LEGACY_GITIGNORE_MARKERS = [
|
|
1183
1198
|
'.root-gitignore-configured-v5',
|
|
@@ -1195,6 +1210,83 @@ async function removeLegacyGitignoreMarkers(devflowDir) {
|
|
|
1195
1210
|
catch { /* ok if absent */ }
|
|
1196
1211
|
}
|
|
1197
1212
|
}
|
|
1213
|
+
/** The most symbolic links followed from the root `.gitignore` to the file it names. */
|
|
1214
|
+
const GITIGNORE_LINK_HOPS = 40;
|
|
1215
|
+
/** A path part that is a `.git`, in any letter case; ASCII only, like the shell twin's `.[Gg][Ii][Tt]`. */
|
|
1216
|
+
const DOT_GIT_PART = /^\.git$/i;
|
|
1217
|
+
/** True when `file` is itself a symbolic link; false for anything else, or nothing, there. */
|
|
1218
|
+
async function isSymbolicLink(file) {
|
|
1219
|
+
try {
|
|
1220
|
+
return (await fs.lstat(file)).isSymbolicLink();
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* The file a write to the root `.gitignore` goes to: `gitignorePath` itself when it is
|
|
1228
|
+
* not a symbolic link; the file the link resolves to when that lies inside `gitRoot`
|
|
1229
|
+
* and outside any `.git` in it; null otherwise, and when the link cannot be followed.
|
|
1230
|
+
*
|
|
1231
|
+
* D-GITIGNORE-LINK-INSIDE: a root .gitignore that is a symbolic link is written only
|
|
1232
|
+
* when the file it resolves to lies inside the project root and outside any .git
|
|
1233
|
+
* folder in the project, the project's own or a nested repository's: no part of its
|
|
1234
|
+
* path below the root may be named .git, in any letter case, as git itself refuses
|
|
1235
|
+
* such a path. Then that file is read and written directly, never through the link.
|
|
1236
|
+
* Reason: a repository can commit .gitignore as a link to any file on the machine, and
|
|
1237
|
+
* the carve-out would be appended to it; a file in a .git is no file of the
|
|
1238
|
+
* repository's either, but git's own hooks and config, and a line appended to a hook
|
|
1239
|
+
* runs as a command the next time git runs it. The name is matched in any case because a
|
|
1240
|
+
* case-insensitive file system (macOS) opens .git for .GIT, and the spelling the link
|
|
1241
|
+
* gave survives resolution: realpath gives only the folders their case on disk, never
|
|
1242
|
+
* the file's own name, and the shell twin's `cd -P` keeps the link's spelling
|
|
1243
|
+
* throughout ({@link DOT_GIT_PART}).
|
|
1244
|
+
* The shell twin, `_erg_resolve_inside` in src/assets/scripts/hooks/ensure-root-gitignore,
|
|
1245
|
+
* applies the same rule the same way: the link is followed one hop at a time, at most
|
|
1246
|
+
* {@link GITIGNORE_LINK_HOPS} hops, a relative target is joined to the folder the link
|
|
1247
|
+
* sits in without normalising it (so `..` is resolved by the file system, as the write
|
|
1248
|
+
* would resolve it), and only the last folder is resolved physically, so a missing
|
|
1249
|
+
* file inside the project is created there as before.
|
|
1250
|
+
*/
|
|
1251
|
+
async function resolveGitignoreTarget(gitRoot, gitignorePath) {
|
|
1252
|
+
if (!(await isSymbolicLink(gitignorePath)))
|
|
1253
|
+
return gitignorePath;
|
|
1254
|
+
let current = path.resolve(gitignorePath);
|
|
1255
|
+
for (let hops = 0; await isSymbolicLink(current); hops++) {
|
|
1256
|
+
if (hops === GITIGNORE_LINK_HOPS)
|
|
1257
|
+
return null;
|
|
1258
|
+
let target;
|
|
1259
|
+
try {
|
|
1260
|
+
target = await fs.readlink(current);
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
if (target === '')
|
|
1266
|
+
return null;
|
|
1267
|
+
current = target.startsWith('/') ? target : `${current.slice(0, current.lastIndexOf('/'))}/${target}`;
|
|
1268
|
+
}
|
|
1269
|
+
const slash = current.lastIndexOf('/');
|
|
1270
|
+
const base = current.slice(slash + 1);
|
|
1271
|
+
if (base === '' || base === '.' || base === '..')
|
|
1272
|
+
return null;
|
|
1273
|
+
let rootReal;
|
|
1274
|
+
let dirReal;
|
|
1275
|
+
try {
|
|
1276
|
+
// fs.promises.realpath is realpath(3), so a `..` after a linked folder goes where
|
|
1277
|
+
// the write would go; the synchronous JS realpath normalises `..` away first.
|
|
1278
|
+
rootReal = await fs.realpath(gitRoot);
|
|
1279
|
+
dirReal = await fs.realpath(current.slice(0, slash) || '/');
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
const rootPrefix = rootReal === '/' ? '/' : `${rootReal}/`;
|
|
1285
|
+
const resolved = `${dirReal === '/' ? '' : dirReal}/${base}`;
|
|
1286
|
+
if (!resolved.startsWith(rootPrefix))
|
|
1287
|
+
return null;
|
|
1288
|
+
return resolved.slice(rootPrefix.length).split('/').some(part => DOT_GIT_PART.test(part)) ? null : resolved;
|
|
1289
|
+
}
|
|
1198
1290
|
/**
|
|
1199
1291
|
* Deterministically ensure the project root .gitignore applies the `.devflow/`
|
|
1200
1292
|
* carve-out (local by default; feature knowledge, conventions.md, the evidence
|
|
@@ -1209,48 +1301,32 @@ async function removeLegacyGitignoreMarkers(devflowDir) {
|
|
|
1209
1301
|
* Called unconditionally (independent of every feature toggle) whenever a git
|
|
1210
1302
|
* root is known.
|
|
1211
1303
|
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
* is how a v5-marked project gains the project line and is re-stamped v6.
|
|
1304
|
+
* Stamps a versioned project-local marker (`.devflow/.root-gitignore-configured-v6`),
|
|
1305
|
+
* the claim the hooks' fast path reads: the shell twin and ensure-devflow-init skip
|
|
1306
|
+
* the carve-out while it stands. The marker is a claim, not proof, so this function
|
|
1307
|
+
* never trusts it: every run re-reads .gitignore and re-runs computeDevflowGitignore.
|
|
1217
1308
|
*
|
|
1218
1309
|
* Idempotent: computeDevflowGitignore returns null for a converged file, so a
|
|
1219
|
-
*
|
|
1220
|
-
* (verbose-logged) — a gitignore write must never abort init.
|
|
1310
|
+
* converged install performs one read and no write. Errors are swallowed
|
|
1311
|
+
* (verbose-logged) — a gitignore write must never abort init. A `.gitignore` that
|
|
1312
|
+
* is a symbolic link leading outside the project, into a `.git`, or nowhere is left
|
|
1313
|
+
* untouched (D-GITIGNORE-LINK-INSIDE, {@link resolveGitignoreTarget}), and nothing is
|
|
1314
|
+
* written or removed under a `.devflow`, or through a marker, that is a symbolic link
|
|
1315
|
+
* (D-CLI-NO-SYMLINK, firstSymbolicLink); either skip is always reported.
|
|
1221
1316
|
*/
|
|
1222
1317
|
export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
1223
1318
|
try {
|
|
1224
1319
|
const devflowDir = path.join(gitRoot, '.devflow');
|
|
1225
1320
|
const markerV6 = path.join(devflowDir, GITIGNORE_MARKER_V6);
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
// read) and run computeDevflowGitignore; write only when it returns non-null.
|
|
1231
|
-
// Idempotent: converged file → computeDevflowGitignore returns null → no write.
|
|
1232
|
-
let v6Marked = false;
|
|
1233
|
-
try {
|
|
1234
|
-
await fs.access(markerV6);
|
|
1235
|
-
v6Marked = true;
|
|
1236
|
-
}
|
|
1237
|
-
catch { /* absent */ }
|
|
1238
|
-
if (v6Marked) {
|
|
1239
|
-
let existingContent = '';
|
|
1240
|
-
try {
|
|
1241
|
-
existingContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
1242
|
-
}
|
|
1243
|
-
catch { /* absent */ }
|
|
1244
|
-
const healContent = computeDevflowGitignore(existingContent);
|
|
1245
|
-
if (healContent !== null) {
|
|
1246
|
-
await fs.writeFile(gitignorePath, healContent, 'utf-8');
|
|
1247
|
-
if (verbose) {
|
|
1248
|
-
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
await removeLegacyGitignoreMarkers(devflowDir);
|
|
1321
|
+
const rootGitignore = path.join(gitRoot, '.gitignore');
|
|
1322
|
+
const gitignorePath = await resolveGitignoreTarget(gitRoot, rootGitignore);
|
|
1323
|
+
if (gitignorePath === null) {
|
|
1324
|
+
p.log.warn(`.gitignore not updated: ${rootGitignore} is a symbolic link that leads outside the project, into a .git, or nowhere; devflow writes nothing through it`);
|
|
1252
1325
|
return;
|
|
1253
1326
|
}
|
|
1327
|
+
// A merge-conflict resolution may have dropped the block from a stamped project,
|
|
1328
|
+
// so the file is always read; it is written only when computeDevflowGitignore
|
|
1329
|
+
// returns non-null, which a converged file never does.
|
|
1254
1330
|
let gitignoreContent = '';
|
|
1255
1331
|
try {
|
|
1256
1332
|
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
@@ -1263,9 +1339,23 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
1263
1339
|
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
|
|
1264
1340
|
}
|
|
1265
1341
|
}
|
|
1266
|
-
//
|
|
1342
|
+
// Everything below writes or removes under .devflow (D-CLI-NO-SYMLINK).
|
|
1343
|
+
const linked = await firstSymbolicLink([devflowDir, markerV6]);
|
|
1344
|
+
if (linked !== null) {
|
|
1345
|
+
p.log.warn(`Nothing written under ${devflowDir}: ${linked} is a symbolic link, and devflow writes nothing through one`);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
// Stamp v6 where nothing stands — an exclusive create never follows a link that
|
|
1349
|
+
// appears at the marker's name, and a marker already there is all the hooks need —
|
|
1350
|
+
// then drop every legacy marker.
|
|
1267
1351
|
await fs.mkdir(devflowDir, { recursive: true });
|
|
1268
|
-
|
|
1352
|
+
try {
|
|
1353
|
+
await fs.writeFile(markerV6, '', { encoding: 'utf-8', flag: 'wx' });
|
|
1354
|
+
}
|
|
1355
|
+
catch (error) {
|
|
1356
|
+
if (!(isNodeSystemError(error) && error.code === 'EEXIST'))
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1269
1359
|
await removeLegacyGitignoreMarkers(devflowDir);
|
|
1270
1360
|
}
|
|
1271
1361
|
catch (error) {
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* installer.ts — a mode flag on this function would have made it a second
|
|
7
7
|
* (design review M2).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* I/O orchestration in src/targets/; pure helpers in src/core/.
|
|
10
|
+
* Warn-not-throw, so one failing artifact never aborts an install.
|
|
11
11
|
*/
|
|
12
12
|
import { promises as fs } from 'fs';
|
|
13
13
|
import * as path from 'path';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devflow-kit",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "A meta-harness for Claude Code — turns a single coding agent into an engineering team: orchestration, parallel review, persistent memory, self-learning, and graph workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,12 +61,9 @@ You receive from orchestrator:
|
|
|
61
61
|
- Cross-reference changed files against EXECUTION_PLAN to identify what's relevant to your task
|
|
62
62
|
- Read those relevant files to understand interfaces, types, naming conventions, error handling, and testing patterns established by prior work
|
|
63
63
|
- If PRIOR_PHASE_SUMMARY is provided, use it to validate your understanding — actual code is authoritative, summaries are supplementary
|
|
64
|
-
- If `DECISIONS_CONTEXT` is provided, follow `devflow:apply-decisions`
|
|
65
|
-
- If `DECISIONS_CONTEXT` is `(none)` or absent: if `.devflow/learning/pitfalls.md` exists, scan for pitfalls in files you're about to modify.
|
|
64
|
+
- If `DECISIONS_CONTEXT` is provided, follow `devflow:apply-decisions` on it. Otherwise read the decisions index — `.devflow/learning/index.md` at the repository's main worktree (`git rev-parse --path-format=absolute --git-common-dir`; when it ends in `/.git` the index lives under its parent) — and follow `devflow:apply-decisions` on it; skip it when absent, empty or `(none)`. State every decision or pitfall you apply in words in code, comments, tests and commit messages, never by its ID.
|
|
66
65
|
- If `HANDOFF_FILE` is provided, read it for prior phase context. Cross-reference against actual code — code is authoritative, handoff is supplementary.
|
|
67
66
|
|
|
68
|
-
When you apply a decision from `.devflow/learning/decisions.md` or avoid a pitfall from `.devflow/learning/pitfalls.md`, cite the entry ID in your final summary (e.g., 'applying ADR-003' or 'per PF-002') so usage can be tracked for capacity reviews.
|
|
69
|
-
|
|
70
67
|
2. **Load domain skills**: Before any analysis, invoke the Skill tool for the domain skills matching the language and stack of the code being touched:
|
|
71
68
|
- `backend` (TypeScript): `Skill(skill="devflow:typescript")`
|
|
72
69
|
- `backend` (Go): `Skill(skill="devflow:go")`
|
|
@@ -82,7 +79,8 @@ When you apply a decision from `.devflow/learning/decisions.md` or avoid a pitfa
|
|
|
82
79
|
|
|
83
80
|
4. **Write tests**: Add tests for new functionality. Cover happy path, error cases, and edge cases. Follow existing test patterns.
|
|
84
81
|
|
|
85
|
-
5. **Run tests**:
|
|
82
|
+
5. **Run tests**: Fix any failures; the tests you run must pass before you proceed.
|
|
83
|
+
**Gate ownership:** Run the targeted tests for your change in its TDD cycle, plus one affected-tests run after your last edit. In a fix mode, compile and run the named failing or regression tests. Never the full suite. Batch fixes: one build check per batch, not per edit. Only Validate runs the full suite.
|
|
86
84
|
|
|
87
85
|
6. **Commit and push**: Create atomic commits with clear messages. Reference TASK_ID. Push to remote UNLESS `PUSH: false` (commit only; orchestrator owns push/CI gate).
|
|
88
86
|
|
|
@@ -128,24 +126,18 @@ When you apply a decision from `.devflow/learning/decisions.md` or avoid a pitfa
|
|
|
128
126
|
|
|
129
127
|
8. **Generate handoff** (if HANDOFF_REQUIRED=true): Include implementation summary for next Code agent (see Output section).
|
|
130
128
|
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
You run builds and tests to verify your own work — including **self-verifying that each fix compiles** when no separate Validate agent runs inside the review pass. A plain `Bash` call defaults to a 120s timeout, and inside a dynamic Workflow a sub-agent that emits no output for 180s is KILLED ("agent stalled"). For any build/test that may run silent longer than ~120s (cold `cargo build`/`cargo test`, large `tsc`, `gradle`, `go build ./...`), do NOT run it as one silent foreground command. Instead:
|
|
129
|
+
## Running commands
|
|
134
130
|
|
|
135
|
-
|
|
136
|
-
1. Run it in the BACKGROUND with the Bash tool (`run_in_background: true`), capturing output + exit code under a unique `<slug>` reused in steps 1–3, e.g. `BASE=/tmp/df-build-<slug>`:
|
|
137
|
-
`<command> > <BASE>.log 2>&1; echo "EXIT=$?" > <BASE>.done`
|
|
138
|
-
Build commands are **NEVER** wrapped in `sh -c`, `bash -c`, or inline interpreters (`python3 -c`, `node -e`) — permission systems deny wrapper-invoked commands that would be allowed directly.
|
|
139
|
-
2. Arm **ONE** Monitor: set `persistent: false`, `timeout_ms` above the expected run time (e.g. 600000), and
|
|
140
|
-
`command: until [ -f <BASE>.done ]; do echo building; sleep 25; done; echo BUILD_DONE; cat <BASE>.done`
|
|
141
|
-
The 25s heartbeat (≪ 180s) keeps you alive past the watchdog.
|
|
142
|
-
- **Exit-code honesty:** the trailing `echo` always exits 0 — the background task's own exit status is meaningless. ALWAYS read the `EXIT=` value written inside `<BASE>.done`.
|
|
143
|
-
- **Bounded polling:** arm ONE Monitor then stop. On timeout, re-arm at most 2× (never more than 3 total Monitor calls per build). After 3 Monitor calls with no finish: record state and escalate — never babysit.
|
|
144
|
-
3. When the monitor reports `BUILD_DONE`: the command PASSED iff `<BASE>.done` contains `EXIT=0`. Read `<BASE>.log`, fix any failures, and only then proceed.
|
|
131
|
+
Run builds, typechecks, lints and tests in the foreground, each with an explicit Bash `timeout` above its expected run time. The ceiling is 600000 ms, or `BASH_MAX_TIMEOUT_MS` when set (`echo ${BASH_MAX_TIMEOUT_MS:-600000}`).
|
|
145
132
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
133
|
+
- Capture, then tail, in one Bash call (shell state does not persist): `LOG=$(mktemp); echo "LOG=$LOG"; <command> >"$LOG" 2>&1; rc=$?; tail -n 40 "$LOG"; echo "EXIT=$rc"`. The printed `EXIT=` value is the result; never decide one from a grep count.
|
|
134
|
+
- Never background a command and wait on it, and never poll across turns: no `sleep` or `true` turns, no sentinel-file checks, no Monitor.
|
|
135
|
+
- Prefer the scoped command for the change (a package, a path or a test file); for the whole set, one workspace-level command over a per-package loop.
|
|
136
|
+
- A run that exceeds its timeout is BLOCKED: report its duration and log path. Do not wait on it, poll it or re-run it.
|
|
137
|
+
- A run expected to exceed the ceiling is split into parts, each under about 90% of it, run in sequence. If it cannot be split, report BLOCKED with the remedy `devflow flags --set bash-max-timeout-ms=<ms>`.
|
|
138
|
+
- Never re-run a command when nothing it reads has changed.
|
|
139
|
+
- Never wrap a build or test command in `sh -c`, `bash -c`, `python3 -c` or `node -e`: permission rules deny wrapped commands they would allow directly.
|
|
140
|
+
- The same rules hold inside a dynamic Workflow sub-agent.
|
|
149
141
|
|
|
150
142
|
## Mode: issue-fix
|
|
151
143
|
|
|
@@ -268,6 +260,8 @@ Return structured completion status:
|
|
|
268
260
|
- {Types to import}
|
|
269
261
|
```
|
|
270
262
|
|
|
263
|
+
Report cap: final message at most about 1,500 tokens; longer material goes to a `mktemp` file (via Bash or Write) and the message gives its path. Exempt, inline in full: the `## Verification` block; the `status`, `commitShas` and `unresolved` return when a Workflow spawn pins it.
|
|
264
|
+
|
|
271
265
|
## Boundaries
|
|
272
266
|
|
|
273
267
|
**Escalate to orchestrator:**
|
|
@@ -23,13 +23,13 @@ The orchestrator provides:
|
|
|
23
23
|
|
|
24
24
|
**Worktree Support**: If `WORKTREE_PATH` is provided, follow the `devflow:worktree-support` skill for path resolution. If omitted, use cwd.
|
|
25
25
|
|
|
26
|
-
- **DECISIONS_CONTEXT** (optional): Compact index of active ADR/PF entries for this
|
|
26
|
+
- **DECISIONS_CONTEXT** (optional): Compact index of active ADR/PF entries for this repository (pre-rendered to `.devflow/learning/index.md` in its main worktree). `(none)` when absent. Use `devflow:apply-decisions` to Read full bodies on demand.
|
|
27
27
|
- **COMPLIANCE_FRAMEWORKS** (compliance focus): `none` (generic controls) or the framework ids in force. Load `references/{id}.md` only for these ids.
|
|
28
28
|
- **FEATURE_KNOWLEDGE** (optional): Pre-computed feature area context for pattern-aware gap analysis. Incorporate feature area patterns and architecture into gap analysis — design additions that fit existing structure. Follow `devflow:apply-feature-knowledge`.
|
|
29
29
|
|
|
30
30
|
## Apply Decisions
|
|
31
31
|
|
|
32
|
-
Follow the `devflow:apply-decisions` skill to scan the `DECISIONS_CONTEXT` index
|
|
32
|
+
Follow the `devflow:apply-decisions` skill to scan the `DECISIONS_CONTEXT` index and Read full ADR/PF bodies on demand. A finding that rests on a decision or pitfall states that rule in words, never its ID: findings feed plans and tickets that are posted to the tracker. Skip when `DECISIONS_CONTEXT` is empty or `(none)`.
|
|
33
33
|
|
|
34
34
|
## Modes
|
|
35
35
|
|
|
@@ -83,6 +83,8 @@ Follow the `devflow:apply-decisions` skill to scan the `DECISIONS_CONTEXT` index
|
|
|
83
83
|
**Overall Assessment**: {BLOCKING | SHOULD-ADDRESS | INFORMATIONAL}
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
Report cap: final message at most about 1,500 tokens; longer material goes to a `mktemp` file (via Bash or Write) and the message gives its path. Exempt, inline in full: the `## Findings` list.
|
|
87
|
+
|
|
86
88
|
## Confidence Scale
|
|
87
89
|
|
|
88
90
|
| Range | Label | Meaning |
|