devflow-kit 3.1.0 → 3.3.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 +52 -0
- package/README.md +2 -2
- package/dist/cli/agents-view/render.js +69 -15
- package/dist/cli/agents-view/state.js +40 -14
- package/dist/cli/commands/agents.js +135 -45
- package/dist/cli/commands/init.js +128 -53
- package/dist/cli/commands/learning.js +61 -13
- package/dist/cli/commands/memory.js +35 -14
- package/dist/cli/commands/uninstall.js +163 -39
- package/dist/commands/code-review.md +1 -3
- package/dist/commands/debug.md +15 -12
- package/dist/commands/dynamic-build.md +172 -135
- package/dist/commands/dynamic-plan.md +9 -3
- package/dist/commands/explore.md +10 -4
- package/dist/commands/implement.md +149 -145
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +8 -2
- package/dist/commands/research.md +8 -2
- package/dist/commands/resolve.md +28 -19
- package/dist/commands/self-review.md +16 -13
- package/dist/core/agent-frontmatter.js +25 -0
- package/dist/core/agent-models.js +201 -36
- package/dist/core/agent-state.js +27 -5
- package/dist/core/assets.js +1 -1
- package/dist/core/feature-config.js +68 -10
- package/dist/core/flags.js +24 -0
- package/dist/core/learning-queue-cleanup.js +10 -11
- package/dist/core/learning-tuning-config.js +8 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/plugins.js +16 -5
- package/dist/core/queue-drain.js +31 -0
- package/dist/hud/components/learning-counts.js +54 -8
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +1 -1
- package/dist/targets/claude-code/installer.js +36 -9
- package/dist/targets/claude-code/post-install.js +128 -38
- package/package.json +1 -1
- package/src/assets/agents/code.md +85 -35
- package/src/assets/agents/design.md +12 -0
- package/src/assets/agents/diagnose.md +18 -11
- package/src/assets/agents/evaluate.md +17 -24
- package/src/assets/agents/knowledge.md +7 -3
- package/src/assets/agents/learning.md +4 -6
- package/src/assets/agents/research.md +21 -0
- package/src/assets/agents/review.md +12 -0
- package/src/assets/agents/scrutinize.md +37 -9
- package/src/assets/agents/simplify.md +24 -0
- package/src/assets/agents/skim.md +6 -2
- package/src/assets/agents/synthesize.md +18 -0
- package/src/assets/agents/test.md +19 -11
- package/src/assets/agents/triage.md +8 -0
- package/src/assets/agents/validate.md +20 -11
- package/src/assets/commands/_partials/_engine.mds +36 -55
- package/src/assets/commands/_partials/_knowledge.mds +1 -3
- package/src/assets/commands/_partials/_plan_contract.mds +1 -1
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/_partials/_wave.mds +8 -6
- package/src/assets/commands/code-review.mds +1 -3
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +126 -72
- package/src/assets/commands/dynamic-plan.mds +7 -1
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +147 -141
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +8 -2
- package/src/assets/commands/research.mds +8 -2
- package/src/assets/commands/resolve.mds +27 -16
- package/src/assets/commands/self-review.mds +15 -10
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +2 -2
- package/src/assets/mds/tracker/_jira.mds +2 -2
- package/src/assets/mds/tracker/_linear.mds +2 -2
- package/src/assets/scripts/ci-wait.cjs +636 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +356 -17
- 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 +4 -3
- package/src/assets/scripts/hooks/ensure-devflow-init +13 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +122 -10
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/json-helper.cjs +12 -145
- package/src/assets/scripts/hooks/json-parse +24 -129
- package/src/assets/scripts/hooks/lib/learning-store.cjs +169 -64
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +1 -1
- 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 +53 -21
- package/src/assets/scripts/hooks/session-start-context +108 -29
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/release-trace.cjs +27 -10
- package/src/assets/skills/accessibility/SKILL.md +1 -1
- package/src/assets/skills/apply-decisions/SKILL.md +12 -82
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +8 -42
- package/src/assets/skills/architecture/SKILL.md +1 -1
- package/src/assets/skills/boundary-validation/SKILL.md +1 -1
- package/src/assets/skills/complexity/SKILL.md +1 -1
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/consistency/SKILL.md +1 -1
- package/src/assets/skills/database/SKILL.md +1 -1
- package/src/assets/skills/dependencies/SKILL.md +1 -1
- package/src/assets/skills/dependency-research/SKILL.md +3 -6
- package/src/assets/skills/design-review/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +1 -1
- package/src/assets/skills/documentation/SKILL.md +1 -1
- package/src/assets/skills/gap-analysis/SKILL.md +1 -1
- package/src/assets/skills/git/SKILL.md +1 -1
- package/src/assets/skills/go/SKILL.md +1 -1
- package/src/assets/skills/java/SKILL.md +1 -1
- package/src/assets/skills/patterns/SKILL.md +1 -1
- package/src/assets/skills/performance/SKILL.md +1 -1
- package/src/assets/skills/python/SKILL.md +1 -1
- package/src/assets/skills/qa/SKILL.md +1 -3
- package/src/assets/skills/quality-gates/SKILL.md +9 -12
- package/src/assets/skills/quality-gates/references/report-template.md +20 -20
- package/src/assets/skills/react/SKILL.md +1 -1
- package/src/assets/skills/regression/SKILL.md +1 -1
- package/src/assets/skills/reliability/SKILL.md +1 -1
- package/src/assets/skills/research-codebase/SKILL.md +1 -1
- package/src/assets/skills/research-competitor/SKILL.md +1 -1
- package/src/assets/skills/research-external/SKILL.md +1 -1
- package/src/assets/skills/research-technology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/rust/SKILL.md +1 -1
- package/src/assets/skills/security/SKILL.md +1 -1
- package/src/assets/skills/software-design/SKILL.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +15 -33
- package/src/assets/skills/testing/SKILL.md +1 -1
- package/src/assets/skills/typescript/SKILL.md +1 -1
- package/src/assets/skills/ui-design/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +3 -55
- package/src/assets/skills/worktree-support/references/discovery.md +48 -0
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file linked-path.ts
|
|
3
|
+
*
|
|
4
|
+
* `firstSymbolicLink`, the check the CLI makes before it writes or deletes under a
|
|
5
|
+
* project's `.devflow/`, or under its `.claude/` when uninstall removes a legacy
|
|
6
|
+
* local install (D-CLI-NO-SYMLINK).
|
|
7
|
+
*/
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
/**
|
|
10
|
+
* The first of `paths` that is itself a symbolic link, or null when none is.
|
|
11
|
+
*
|
|
12
|
+
* D-CLI-NO-SYMLINK: the CLI writes or deletes under a project's `.devflow/` only
|
|
13
|
+
* where neither `.devflow` nor the entry it acts on through it is a symbolic link:
|
|
14
|
+
* `devflow init` stamps its carve-out marker, removes the legacy markers and
|
|
15
|
+
* writes `.devflow/config.json` only then, `devflow learning --configure` writes
|
|
16
|
+
* the project's `learning.json` only then, and the queue drains (`devflow learning
|
|
17
|
+
* --clear|--disable`, `devflow memory --disable|--clear` and `devflow init
|
|
18
|
+
* --no-learning|--no-memory`) delete nothing when `.devflow` or the queue's folder
|
|
19
|
+
* is one. `devflow uninstall`, removing a legacy local install, deletes or rewrites
|
|
20
|
+
* nothing under the project's `.devflow` or `.claude` where either, or anything
|
|
21
|
+
* below it on the way to what it removes, is one (legacyLocalChangeGuard in
|
|
22
|
+
* uninstall.ts). Reason: a repository can commit `.devflow`, or a folder or file in
|
|
23
|
+
* it, as a link to any place on the machine, and a write or delete through one lands
|
|
24
|
+
* wherever it points. The hooks hold the same rule (D-HOOKS-NO-SYMLINK, git-marker)
|
|
25
|
+
* and so do the learning ops (D-NO-LINKED-TREE). Only paths below the project root
|
|
26
|
+
* are passed here: the root and the folders above it are the user's choice.
|
|
27
|
+
*
|
|
28
|
+
* Each path is checked with lstat, so a link is seen rather than followed. Nothing at
|
|
29
|
+
* a path, or a file where a folder was expected on the way to it, is no link; any
|
|
30
|
+
* other lstat failure is thrown, so a caller that cannot check acts on nothing.
|
|
31
|
+
*/
|
|
32
|
+
export async function firstSymbolicLink(paths) {
|
|
33
|
+
for (const candidate of paths) {
|
|
34
|
+
try {
|
|
35
|
+
if ((await fs.lstat(candidate)).isSymbolicLink())
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const code = error.code;
|
|
40
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=linked-path.js.map
|
package/dist/core/plugins.js
CHANGED
|
@@ -72,6 +72,10 @@ export const DEVFLOW_PLUGINS = [
|
|
|
72
72
|
commands: ['/plan'],
|
|
73
73
|
agents: ['git', 'skim', 'synthesize', 'design'],
|
|
74
74
|
skills: ['gap-analysis', 'design-review', 'patterns', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'],
|
|
75
|
+
// D-CHARTER-BOUNDED-INLINE: /plan's main thread orchestrates and loads no
|
|
76
|
+
// companion skills, so `requires:` lists only skills this plugin's command,
|
|
77
|
+
// agents and skills reference. `software-design` and `test-driven-development`
|
|
78
|
+
// have no reader here; the closure guard's reverse arm rejects an unread entry.
|
|
75
79
|
requires: [
|
|
76
80
|
'apply-decisions',
|
|
77
81
|
'architecture',
|
|
@@ -87,8 +91,6 @@ export const DEVFLOW_PLUGINS = [
|
|
|
87
91
|
'reliability',
|
|
88
92
|
'review-methodology',
|
|
89
93
|
'security',
|
|
90
|
-
'software-design',
|
|
91
|
-
'test-driven-development',
|
|
92
94
|
'testing',
|
|
93
95
|
],
|
|
94
96
|
rules: [],
|
|
@@ -127,7 +129,11 @@ export const DEVFLOW_PLUGINS = [
|
|
|
127
129
|
commands: ['/code-review'],
|
|
128
130
|
agents: ['git', 'review', 'synthesize'],
|
|
129
131
|
skills: ['architecture', 'complexity', 'consistency', 'database', 'dependencies', 'documentation', 'performance', 'regression', 'reliability', 'review-methodology', 'security', 'testing', 'worktree-support', 'apply-feature-knowledge'],
|
|
130
|
-
|
|
132
|
+
// D-CHARTER-BOUNDED-INLINE: /code-review's main thread orchestrates and loads
|
|
133
|
+
// no companion skills, so `requires:` lists only skills this plugin's command,
|
|
134
|
+
// agents and skills reference. `quality-gates` and `software-design` have no
|
|
135
|
+
// reader here; the closure guard's reverse arm rejects an unread entry.
|
|
136
|
+
requires: ['apply-decisions', 'docs-framework', 'git'],
|
|
131
137
|
rules: [],
|
|
132
138
|
},
|
|
133
139
|
{
|
|
@@ -238,7 +244,10 @@ export const DEVFLOW_PLUGINS = [
|
|
|
238
244
|
commands: ['/release'],
|
|
239
245
|
agents: ['git', 'validate'],
|
|
240
246
|
skills: ['git', 'worktree-support'],
|
|
241
|
-
|
|
247
|
+
// D-PRELOAD-TRIM: nothing in this plugin's corpus reads `testing` (Validate
|
|
248
|
+
// preloads only `worktree-support`), so the closure guard's reverse arm holds
|
|
249
|
+
// this list empty.
|
|
250
|
+
requires: [],
|
|
242
251
|
rules: [],
|
|
243
252
|
},
|
|
244
253
|
{
|
|
@@ -247,7 +256,9 @@ export const DEVFLOW_PLUGINS = [
|
|
|
247
256
|
commands: ['/self-review'],
|
|
248
257
|
agents: ['simplify', 'scrutinize', 'validate', 'knowledge'],
|
|
249
258
|
skills: ['quality-gates', 'software-design', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'],
|
|
250
|
-
|
|
259
|
+
// D-PRELOAD-TRIM: no agent, command or skill in this plugin's corpus names
|
|
260
|
+
// `testing` (Validate preloads only `worktree-support`), so it is not required here.
|
|
261
|
+
requires: ['apply-decisions'],
|
|
251
262
|
rules: [],
|
|
252
263
|
},
|
|
253
264
|
{
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file queue-drain.ts
|
|
3
|
+
*
|
|
4
|
+
* `drainQueueFiles`, the delete the queue drains share — `drainLearningQueue`
|
|
5
|
+
* (learning-queue-cleanup.ts) and `drainMemoryQueue` (cli/commands/memory.ts) —
|
|
6
|
+
* and `formatRefusedDrain`, the line the CLI prints for a drain it refused.
|
|
7
|
+
*/
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import { firstSymbolicLink } from './linked-path.js';
|
|
10
|
+
/**
|
|
11
|
+
* Delete each of `files` unless one of `folders` — `.devflow` and the folder that
|
|
12
|
+
* holds the files, outermost first — is a symbolic link (D-CLI-NO-SYMLINK): through a
|
|
13
|
+
* linked folder, each delete would remove a same-named file wherever the link points.
|
|
14
|
+
* A file that is already gone is not an error; any other failure propagates, as the
|
|
15
|
+
* check's own does, so the command reports it.
|
|
16
|
+
*/
|
|
17
|
+
export async function drainQueueFiles(folders, files) {
|
|
18
|
+
const linkedFolder = await firstSymbolicLink(folders);
|
|
19
|
+
if (linkedFolder !== null)
|
|
20
|
+
return { drained: false, linkedFolder };
|
|
21
|
+
await Promise.all(files.map(file => fs.unlink(file).catch((error) => {
|
|
22
|
+
if (error.code !== 'ENOENT')
|
|
23
|
+
throw error;
|
|
24
|
+
})));
|
|
25
|
+
return { drained: true };
|
|
26
|
+
}
|
|
27
|
+
/** The warning for a drain refused at `linkedFolder`: which queue kept its files, and why. */
|
|
28
|
+
export function formatRefusedDrain(queue, linkedFolder) {
|
|
29
|
+
return `The ${queue} queue was not drained: ${linkedFolder} is a symbolic link, and devflow deletes nothing through one`;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=queue-drain.js.map
|
|
@@ -5,6 +5,55 @@ import { getLedgerRoot } from '../../core/ledger-root.js';
|
|
|
5
5
|
import { isActiveDecisionsStatus } from '../../core/observations.js';
|
|
6
6
|
/** The HUD's per-git-command budget (src/hud/git.ts GIT_TIMEOUT). */
|
|
7
7
|
const LEDGER_ROOT_TIMEOUT_MS = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* The largest ledger the statusline reads: 8 MiB, far above the roughly 0.5 MB
|
|
10
|
+
* real ledgers reach.
|
|
11
|
+
*
|
|
12
|
+
* D-HUD-LEDGER-BOUNDED: the statusline counts the ledger only when it is a regular
|
|
13
|
+
* file of at most LEDGER_MAX_BYTES, and never reads it if it is a symbolic link; a
|
|
14
|
+
* link, any other kind of file or a larger one shows no counts, as an absent
|
|
15
|
+
* ledger does. Reason: the statusline reads the ledger on every prompt, and a
|
|
16
|
+
* repository can commit it as a link to an endless source such as /dev/zero, or
|
|
17
|
+
* as a huge file, either of which would hang the statusline.
|
|
18
|
+
*/
|
|
19
|
+
export const LEDGER_MAX_BYTES = 8 * 1024 * 1024;
|
|
20
|
+
/**
|
|
21
|
+
* The ledger's text, or null when D-HUD-LEDGER-BOUNDED refuses it or it cannot be
|
|
22
|
+
* read. lstat refuses a link without following it, the open refuses one that took
|
|
23
|
+
* the ledger's place since (O_NOFOLLOW) and never blocks on a FIFO (O_NONBLOCK),
|
|
24
|
+
* and the read takes at most the size fstat checked.
|
|
25
|
+
*/
|
|
26
|
+
function readBoundedLedger(ledgerPath) {
|
|
27
|
+
let fd;
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.lstatSync(ledgerPath).isFile())
|
|
30
|
+
return null;
|
|
31
|
+
fd = fs.openSync(ledgerPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const stat = fs.fstatSync(fd);
|
|
38
|
+
if (!stat.isFile() || stat.size > LEDGER_MAX_BYTES)
|
|
39
|
+
return null;
|
|
40
|
+
const buf = Buffer.alloc(stat.size);
|
|
41
|
+
let total = 0;
|
|
42
|
+
while (total < buf.length) {
|
|
43
|
+
const read = fs.readSync(fd, buf, total, buf.length - total, total);
|
|
44
|
+
if (read === 0)
|
|
45
|
+
break;
|
|
46
|
+
total += read;
|
|
47
|
+
}
|
|
48
|
+
return buf.toString('utf-8', 0, total);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
fs.closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
8
57
|
function isLedgerCountRow(val) {
|
|
9
58
|
if (typeof val !== 'object' || val === null)
|
|
10
59
|
return false;
|
|
@@ -17,17 +66,14 @@ function isLedgerCountRow(val) {
|
|
|
17
66
|
}
|
|
18
67
|
/**
|
|
19
68
|
* Read .devflow/learning/decisions-ledger.jsonl and count active anchored
|
|
20
|
-
* rows by type. Returns null if the ledger is missing
|
|
21
|
-
* (graceful fallback). Exported for
|
|
69
|
+
* rows by type. Returns null if the ledger is missing, is refused by
|
|
70
|
+
* D-HUD-LEDGER-BOUNDED, or holds no valid rows (graceful fallback). Exported for
|
|
71
|
+
* use by the main HUD entry point.
|
|
22
72
|
*/
|
|
23
73
|
export function gatherLearningCounts(cwd) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
content = fs.readFileSync(getDecisionsLedgerPath(cwd), 'utf-8');
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
74
|
+
const content = readBoundedLedger(getDecisionsLedgerPath(cwd));
|
|
75
|
+
if (content === null)
|
|
29
76
|
return null;
|
|
30
|
-
}
|
|
31
77
|
const counts = { decisions: 0, pitfalls: 0 };
|
|
32
78
|
let parsedAny = false;
|
|
33
79
|
for (const rawLine of content.split('\n')) {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Load when the resolved tracker provider is `github` and the operation is `create-release`.
|
|
4
4
|
|
|
5
|
-
**Mechanics held here:** the
|
|
5
|
+
**Mechanics held here:** the shipped-issues step only. Tag creation, release creation and notes composition stay with the operation.
|
|
6
6
|
|
|
7
7
|
### Process
|
|
8
8
|
|
|
9
9
|
Inside step 5 (compose release notes):
|
|
10
10
|
|
|
11
|
-
- If `SHIPPED_ISSUES` provided: append a `##
|
|
11
|
+
- If `SHIPPED_ISSUES` provided: append a `## Shipped Issues` section with issue references — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails)
|
|
@@ -7,7 +7,7 @@ Load when the resolved tracker provider is `github` and the operation is `gather
|
|
|
7
7
|
### Process
|
|
8
8
|
|
|
9
9
|
1a. **Last release tag.** Step 1's `git describe` can return a non-release marker tag. From `WORKTREE_PATH` (else cwd), run `node "$HOME/.devflow/scripts/release-trace.cjs" last-tag`: `LAST_TAG <tag>` ⇒ that tag is `{last_tag}`; `LAST_TAG none` ⇒ step 1's initial-commit rule; anything else ⇒ keep step 1's tag and report status `INDETERMINATE (last release tag unresolved)`.
|
|
10
|
-
3a. **Closing-keyword rule.** A candidate follows, on the same line, a whitespace token matching `^\(?(close[sd]?|fix(e[sd])?|resolve[sd]
|
|
10
|
+
3a. **Closing-keyword rule.** A candidate follows, on the same line, a whitespace token matching `^\(?(close[sd]?|fix(e[sd])?|resolve[sd]?):?$` (case-insensitive). Take the next token, plus each further token while the previous one ends in `,`. Split each on `,`, strip one leading `(` and every trailing character in `[.,;:)\]!?]`, drop empties, then apply step 5's anchored gate unchanged. Read each message as `git log --format=%B` lines.
|
|
11
11
|
3b. **This provider's history grammar** is `^#[1-9][0-9]{0,8}$`. A bare number is not a reference here either: a keyword-anchored candidate must carry the `#`, and step 4 renders every number it reads from a merged PR as `#{n}` before step 5's gate.
|
|
12
12
|
4. If `gh` is authenticated and remote is reachable, resolve which issues the range's merged PRs close — **one listing, never one call per commit** — and merge the result with the commit-message set:
|
|
13
13
|
- **Once, before the listing:** this repo's identity, `gh api 'repos/{owner}/{repo}' --jq '.owner.login + "/" + .name'`, and the tag's UTC date, `TAG_DATE=$(TZ=UTC git log -1 --date=format-local:%Y-%m-%d --format=%cd {last_tag})`, shape-gated `^[0-9]{4}-[0-9]{2}-[0-9]{2}$` — a local date can drop a PR merged just after the tag. If `TAG_DATE` fails this gate, treat it as **the listing fails** below: emit `TRACEABILITY: DEGRADED ({reason})` and go straight to that bullet's ≤25-PR fallback.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Load when the resolved tracker provider is `jira` and the operation is `create-release`.
|
|
4
4
|
|
|
5
|
-
**Mechanics held here:** the
|
|
5
|
+
**Mechanics held here:** the shipped-issues step only. Tag creation, release creation and notes composition stay with the operation and are unchanged — they are release-host mechanics, not tracker mechanics.
|
|
6
6
|
|
|
7
7
|
### Process
|
|
8
8
|
|
|
9
9
|
Inside step 5 (compose release notes):
|
|
10
10
|
|
|
11
|
-
- If `SHIPPED_ISSUES` is provided: append a `##
|
|
11
|
+
- If `SHIPPED_ISSUES` is provided: append a `## Shipped Issues` section rendering each entry through `## Reference Rendering` — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails).
|
|
12
12
|
- Pre-flight the list against `^[A-Z][A-Z0-9_]{1,9}-[1-9][0-9]{0,8}$`, anchored at both ends, and **drop** every entry that fails, reporting each as `TRACEABILITY: DEGRADED (issue reference "{ref}" does not match jira reference grammar)` — a bare number among them goes with them, in silence: `ambiguous issue reference` answers a reference a caller named, not one line of a list nobody chose. Every entry dropped ⇒ `TRACEABILITY: DEGRADED (no parseable refs for provider {p})` and the section is omitted rather than rendered empty.
|
|
13
13
|
- `## Reference Rendering` absent, or its token discarded by the gate below ⇒ render the key itself on its own line, and record the discard under `### Substitutions`.
|
|
14
14
|
|
|
@@ -7,7 +7,7 @@ Load when the resolved tracker provider is `jira` and the operation is `gather-r
|
|
|
7
7
|
### Process
|
|
8
8
|
|
|
9
9
|
1a. **Last release tag.** Step 1's `git describe` can return a non-release marker tag. From `WORKTREE_PATH` (else cwd), run `node "$HOME/.devflow/scripts/release-trace.cjs" last-tag`: `LAST_TAG <tag>` ⇒ that tag is `{last_tag}`; `LAST_TAG none` ⇒ step 1's initial-commit rule; anything else ⇒ keep step 1's tag and report status `INDETERMINATE (last release tag unresolved)`.
|
|
10
|
-
3a. **Closing-keyword rule.** A candidate follows, on the same line, a whitespace token matching `^\(?(close[sd]?|fix(e[sd])?|resolve[sd]
|
|
10
|
+
3a. **Closing-keyword rule.** A candidate follows, on the same line, a whitespace token matching `^\(?(close[sd]?|fix(e[sd])?|resolve[sd]?):?$` (case-insensitive). Take the next token, plus each further token while the previous one ends in `,`. Split each on `,`, strip one leading `(` and every trailing character in `[.,;:)\]!?]`, drop empties, then apply step 5's anchored gate unchanged. Read each message as `git log --format=%B` lines.
|
|
11
11
|
4. Resolve which issues the commit range closes:
|
|
12
12
|
- **There is no closing-reference capability on this provider.** Emit `TRACEABILITY: DEGRADED (unsupported by jira)` once for the whole step and fall back to the commit-message set alone — the refs parsed out of the candidate references the agent extracted from the range's commit messages.
|
|
13
13
|
- **This provider's history grammar** is `^[A-Z][A-Z0-9_]{1,9}-[1-9][0-9]{0,8}$`, and its KEY segment must equal the resolved project key after ASCII-upper normalisation — a well-formed key belonging to another project is a `TRACEABILITY: DEGRADED (foreign issue reference {ref})`, not a shipped issue.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Load when the resolved tracker provider is `linear` and the operation is `create-release`.
|
|
4
4
|
|
|
5
|
-
**Mechanics held here:** the
|
|
5
|
+
**Mechanics held here:** the shipped-issues step only. Tag creation, release creation and notes composition stay with the operation and are unchanged — they are release-host mechanics, not tracker mechanics.
|
|
6
6
|
|
|
7
7
|
### Process
|
|
8
8
|
|
|
9
9
|
Inside step 5 (compose release notes):
|
|
10
10
|
|
|
11
|
-
- If `SHIPPED_ISSUES` is provided: append a `##
|
|
11
|
+
- If `SHIPPED_ISSUES` is provided: append a `## Shipped Issues` section rendering each entry through `## Reference Rendering` — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails).
|
|
12
12
|
- **ASCII-upper-normalise every entry first.** Pre-flight the list against **either** anchored form — the team-key form `^[A-Z][A-Z0-9]{0,9}-[1-9][0-9]{0,8}$` or the internal-id form `^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$`, anchored at both ends, never joined into one alternation, and **drop** every entry that fails, reporting each as `TRACEABILITY: DEGRADED (issue reference "{ref}" does not match linear reference grammar)` — a bare number among them goes with them, in silence: `ambiguous issue reference` answers a reference a caller named, not one line of a list nobody chose. Every entry dropped ⇒ `TRACEABILITY: DEGRADED (no parseable refs for provider {p})` and the section is omitted rather than rendered empty.
|
|
13
13
|
- `## Reference Rendering` absent, or its token discarded by the gate below ⇒ render the reference itself on its own line, and record the discard under `### Substitutions`.
|
|
14
14
|
|
|
@@ -7,7 +7,7 @@ Load when the resolved tracker provider is `linear` and the operation is `gather
|
|
|
7
7
|
### Process
|
|
8
8
|
|
|
9
9
|
1a. **Last release tag.** Step 1's `git describe` can return a non-release marker tag. From `WORKTREE_PATH` (else cwd), run `node "$HOME/.devflow/scripts/release-trace.cjs" last-tag`: `LAST_TAG <tag>` ⇒ that tag is `{last_tag}`; `LAST_TAG none` ⇒ step 1's initial-commit rule; anything else ⇒ keep step 1's tag and report status `INDETERMINATE (last release tag unresolved)`.
|
|
10
|
-
3a. **Closing-keyword rule.** A candidate follows, on the same line, a whitespace token matching `^\(?(close[sd]?|fix(e[sd])?|resolve[sd]
|
|
10
|
+
3a. **Closing-keyword rule.** A candidate follows, on the same line, a whitespace token matching `^\(?(close[sd]?|fix(e[sd])?|resolve[sd]?):?$` (case-insensitive). Take the next token, plus each further token while the previous one ends in `,`. Split each on `,`, strip one leading `(` and every trailing character in `[.,;:)\]!?]`, drop empties, then apply step 5's anchored gate unchanged. Read each message as `git log --format=%B` lines.
|
|
11
11
|
4. Resolve which issues the commit range closes:
|
|
12
12
|
- **There is no closing-reference capability on this provider.** Emit `TRACEABILITY: DEGRADED (unsupported by linear)` once for the whole step and fall back to the commit-message set alone — the references parsed out of the candidate references the agent extracted from the range's commit messages. The magic words this provider recognises in a pull-request body are the SERVER's own behaviour and are not a capability this operation can read back: a body that closed an issue leaves no signal here, which is precisely why this step degrades instead of guessing.
|
|
13
13
|
- **This provider's history grammar is the TEAM-KEY form only** — `^[A-Z][A-Z0-9]{0,9}-[1-9][0-9]{0,8}$`, with its key segment equal to the resolved team key after ASCII-upper normalisation; a well-formed reference on another team is a `TRACEABILITY: DEGRADED (foreign issue reference {ref})`. The internal-id form is NOT admitted from history: a bare identifier carries no team, so nothing distinguishes one workspace's from another's.
|
|
@@ -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,
|
|
@@ -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).
|
|
@@ -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 &&
|
|
@@ -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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devflow-kit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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": {
|