devflow-kit 2.0.1 → 2.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.
@@ -1,10 +1,69 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
3
  import { LEGACY_PLUGIN_NAMES, DELETED_PLUGIN_NAMES } from './plugins.js';
4
- import { VIEW_MODES } from './flags.js';
4
+ import { VIEW_MODES, migrateLegacyFlagsToRecord, sanitizeFlagsRecord, } from './flags.js';
5
5
  import { normalizeComplianceFeature } from './compliance.js';
6
+ import { writeFileAtomicExclusive } from './fs-atomic.js';
7
+ /**
8
+ * Parse features.flags across the three on-disk shapes; reports whether a heal is owed.
9
+ *
10
+ * Case A: string[] — legacy format, migrated to FlagsRecord (fold viewMode in).
11
+ * Case B: object — already a FlagsRecord, fold lingering viewMode if present.
12
+ * Case C: missing/other — default to empty record.
13
+ *
14
+ * The returned `legacy` flag is true only for Case A (array). Callers use it as the
15
+ * flags-specific clause of needsHeal, keeping the legacy-artifact knowledge in one
16
+ * place and letting needsHeal derive from the parse result instead of re-inspecting
17
+ * features.flags after the fact.
18
+ */
19
+ function parseManifestFlags(features, knownFlags) {
20
+ const rawFlags = features.flags;
21
+ if (Array.isArray(rawFlags)) {
22
+ // Case A: string[] → FlagsRecord migration.
23
+ // Filter to strings only (malformed elements are silently dropped).
24
+ const enabledIds = rawFlags.filter(e => typeof e === 'string');
25
+ // Extract legacyViewMode for the migration fold.
26
+ const rawViewMode = features.viewMode;
27
+ const legacyViewMode = typeof rawViewMode === 'string' && VIEW_MODES.includes(rawViewMode)
28
+ ? rawViewMode
29
+ : undefined;
30
+ return { flags: migrateLegacyFlagsToRecord(enabledIds, knownFlags, legacyViewMode), legacy: true };
31
+ }
32
+ if (rawFlags !== null && typeof rawFlags === 'object') {
33
+ // Case B: already a FlagsRecord. Spread to avoid mutating the parsed value.
34
+ // Single cast: rawFlags is already confirmed to be a non-null, non-array object.
35
+ // sanitizeFlagsRecord (called by the outer readManifest) validates all values,
36
+ // dropping invalid ones — so the double assertion is unnecessary here (applies TS-M3).
37
+ const flagsRecord = { ...rawFlags };
38
+ // Fold lingering viewMode into flags['view-mode'] when the record lacks a
39
+ // non-default value (e.g. a manifest written by an older init that stored viewMode
40
+ // as a separate deprecated field alongside a FlagsRecord with view-mode:null).
41
+ const rawViewMode = features.viewMode;
42
+ if (typeof rawViewMode === 'string' && VIEW_MODES.includes(rawViewMode)) {
43
+ const existing = flagsRecord['view-mode'];
44
+ if (existing === null || existing === undefined || existing === 'default') {
45
+ flagsRecord['view-mode'] = rawViewMode;
46
+ }
47
+ }
48
+ return { flags: flagsRecord, legacy: false };
49
+ }
50
+ // Case C: missing/malformed → empty record
51
+ return { flags: {}, legacy: false };
52
+ }
6
53
  /**
7
54
  * Read and parse the manifest file. Returns null if missing or corrupt.
55
+ *
56
+ * Self-heals the following on-disk inconsistencies (applies ADR-014):
57
+ * - features.kb → features.knowledge rename
58
+ * - features.decisions → features.learning rename
59
+ * - features.flags as string[] → FlagsRecord (via migrateLegacyFlagsToRecord)
60
+ * - features.viewMode folded into flags['view-mode'] and stripped from result
61
+ * - features.knownFlags stripped from result (folded into FlagsRecord key-presence)
62
+ * - features.proxy absent → false
63
+ * - features.compliance absent/malformed → {enabled:false, frameworks:[]}
64
+ *
65
+ * D39: heal-write failure returns the migrated in-memory manifest (not null).
66
+ * The on-disk format remains unhealed; next read triggers another attempt.
8
67
  */
9
68
  export async function readManifest(devflowDir) {
10
69
  const manifestPath = path.join(devflowDir, 'manifest.json');
@@ -32,14 +91,27 @@ export async function readManifest(devflowDir) {
32
91
  const learning = typeof features.learning === 'boolean' ? features.learning
33
92
  : typeof features.decisions === 'boolean' ? features.decisions
34
93
  : false;
35
- const needsHeal = features.kb !== undefined || features.decisions !== undefined;
36
- const SECURITY_MODES = ['none', 'user', 'managed'];
37
94
  // Self-heal: non-string-array or absent knownFlags/knownPlugins → undefined (never partial/garbage)
38
95
  const asStringArray = (val) => Array.isArray(val) && val.every(e => typeof e === 'string')
39
96
  ? val
40
97
  : undefined;
41
- const knownFlags = asStringArray(features.knownFlags);
42
98
  const knownPlugins = asStringArray(data.knownPlugins);
99
+ // knownFlags is consumed here for migration; NOT carried into the returned manifest
100
+ const knownFlags = asStringArray(features.knownFlags);
101
+ // ── Parse flags ────────────────────────────────────────────────────────────
102
+ // Delegates to parseManifestFlags (three cases: A=string[], B=object, C=missing).
103
+ // `flagsWereLegacy` is true only when the on-disk shape was a string[] (Case A),
104
+ // keeping the needsHeal predicate in lockstep with the parse branch above.
105
+ const { flags: parsedFlags, legacy: flagsWereLegacy } = parseManifestFlags(features, knownFlags);
106
+ // PF-023 + D39: sanitize all values; block prototype pollution keys.
107
+ const sanitizedFlags = sanitizeFlagsRecord(parsedFlags);
108
+ // needsHeal when any legacy artifact is present on disk
109
+ const needsHeal = features.kb !== undefined ||
110
+ features.decisions !== undefined ||
111
+ flagsWereLegacy ||
112
+ features.knownFlags !== undefined ||
113
+ features.viewMode !== undefined;
114
+ const SECURITY_MODES = ['none', 'user', 'managed'];
43
115
  const manifest = {
44
116
  version: data.version,
45
117
  plugins: data.plugins,
@@ -52,25 +124,34 @@ export async function readManifest(devflowDir) {
52
124
  knowledge,
53
125
  learning,
54
126
  rules: typeof features.rules === 'boolean' ? features.rules : true,
55
- flags: Array.isArray(features.flags) ? features.flags : [],
56
- knownFlags,
57
- viewMode: typeof features.viewMode === 'string' && VIEW_MODES.includes(features.viewMode)
58
- ? features.viewMode
59
- : undefined,
127
+ flags: sanitizedFlags,
128
+ // knownFlags and viewMode are NOT carried into the result.
129
+ // - knownFlags: its semantic (which flags are "known") is encoded in
130
+ // FlagsRecord key-presence (present key = known, absent = new/adopt-on-seed).
131
+ // - viewMode: folded into flags['view-mode'] above.
132
+ // Leaving them out prevents them from being echoed back on the next write
133
+ // and causing a spurious needsHeal on every read.
60
134
  security: typeof features.security === 'string' && SECURITY_MODES.includes(features.security)
61
135
  ? features.security
62
136
  : undefined,
63
137
  // Self-heal: absent proxy field defaults to false (applies ADR-014 self-heal idiom)
64
138
  proxy: typeof features.proxy === 'boolean' ? features.proxy : false,
65
139
  // Self-heal: absent/malformed compliance → {enabled:false, frameworks:[]}
66
- // (applies ADR-014 self-heal idiom; normalizeComplianceFeature is in TOLERANT section)
67
140
  compliance: normalizeComplianceFeature(features.compliance),
68
141
  },
69
142
  installedAt: data.installedAt,
70
143
  updatedAt: data.updatedAt,
71
144
  };
72
145
  if (needsHeal) {
73
- await writeManifest(devflowDir, manifest);
146
+ // D39: wrap the heal-write in its own try/catch so a write failure does NOT
147
+ // propagate to the caller. The migrated in-memory manifest is returned even
148
+ // when the on-disk heal fails. The next read will retry the heal.
149
+ try {
150
+ await writeManifest(devflowDir, manifest);
151
+ }
152
+ catch {
153
+ // heal-write failed — return migrated in-memory manifest unchanged
154
+ }
74
155
  }
75
156
  return manifest;
76
157
  }
@@ -79,12 +160,15 @@ export async function readManifest(devflowDir) {
79
160
  }
80
161
  }
81
162
  /**
82
- * Write manifest to disk. Creates parent directory if needed.
163
+ * Write manifest to disk atomically. Creates parent directory if needed.
164
+ *
165
+ * Uses writeFileAtomicExclusive (tmp+rename) to prevent readers from seeing
166
+ * partial writes. Applies D34 atomic-write semantics across all manifest updates.
83
167
  */
84
168
  export async function writeManifest(devflowDir, data) {
85
169
  await fs.mkdir(devflowDir, { recursive: true });
86
170
  const manifestPath = path.join(devflowDir, 'manifest.json');
87
- await fs.writeFile(manifestPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
171
+ await writeFileAtomicExclusive(manifestPath, JSON.stringify(data, null, 2) + '\n');
88
172
  }
89
173
  /**
90
174
  * Update a single feature field in the manifest. No-op when no manifest exists.
@@ -2,8 +2,8 @@
2
2
  * Strip `teammateMode: "auto"` from a freshly parsed copy of the settings JSON.
3
3
  * Returns the serialised JSON string (with trailing newline).
4
4
  *
5
- * Pure string→string — matches the pipeline pattern used by stripFlags /
6
- * stripViewMode so uninstall.ts can chain it without a separate parse/stringify.
5
+ * Pure string→string — matches the pipeline pattern used by stripFlags so
6
+ * uninstall.ts can chain it without a separate parse/stringify.
7
7
  * Only removes the key when the value is exactly `"auto"`; user-set values
8
8
  * (`"tmux"`, `"in-process"`, etc.) are preserved as-is.
9
9
  *
@@ -1,73 +1,9 @@
1
1
  /**
2
- * ANSI color helpers — no dependencies, precompiled escape sequences.
3
- * Used by HUD components for direct terminal output (not @clack/prompts).
2
+ * ANSI color helpers — re-exported from src/core/ansi.ts.
3
+ *
4
+ * The canonical implementation lives in src/core/ansi.ts (agent-neutral home,
5
+ * applies ADR-013). This file is a re-export barrel so all existing HUD
6
+ * component call sites continue to resolve `../colors.js` without change.
4
7
  */
5
- const ESC = '\x1b[';
6
- const RESET = `${ESC}0m`;
7
- export function bold(s) {
8
- return `${ESC}1m${s}${RESET}`;
9
- }
10
- export function dim(s) {
11
- return `${ESC}2m${s}${RESET}`;
12
- }
13
- export function red(s) {
14
- return `${ESC}31m${s}${RESET}`;
15
- }
16
- export function green(s) {
17
- return `${ESC}32m${s}${RESET}`;
18
- }
19
- export function yellow(s) {
20
- return `${ESC}33m${s}${RESET}`;
21
- }
22
- export function blue(s) {
23
- return `${ESC}34m${s}${RESET}`;
24
- }
25
- export function magenta(s) {
26
- return `${ESC}35m${s}${RESET}`;
27
- }
28
- export function cyan(s) {
29
- return `${ESC}36m${s}${RESET}`;
30
- }
31
- export function gray(s) {
32
- return `${ESC}90m${s}${RESET}`;
33
- }
34
- export function white(s) {
35
- return `${ESC}37m${s}${RESET}`;
36
- }
37
- export function orange(s) {
38
- return `${ESC}38;5;208m${s}${RESET}`;
39
- }
40
- export function brightRed(s) {
41
- return `${ESC}91m${s}${RESET}`;
42
- }
43
- export function boldRed(s) {
44
- return `${ESC}1;31m${s}${RESET}`;
45
- }
46
- export function bgGreen(s) {
47
- return `${ESC}42m${s}${RESET}`;
48
- }
49
- export function bgYellow(s) {
50
- return `${ESC}43m${s}${RESET}`;
51
- }
52
- export function bgRed(s) {
53
- return `${ESC}41m${s}${RESET}`;
54
- }
55
- export function truncate(s, max) {
56
- return s.length > max ? s.slice(0, max - 1) + '\u2026' : s;
57
- }
58
- // S2 — Terminal-escape and control-character sanitization (HIGH, pre-existing defect).
59
- //
60
- // The prior pattern (/\x1b\[[0-9;]*m/g) matched only SGR sequences (colour).
61
- // The broadened ANSI_PATTERN also covers:
62
- // CSI sequences — \x1b[ ... with intermediate bytes, any final byte
63
- // OSC sequences — \x1b] ... terminated by BEL (\x07) or ST (\x1b\\)
64
- // Two-byte C1 — \x1b followed by any single character in the C1 range
65
- // CTRL_PATTERN removes non-printable C0 control chars that are not TAB (\x09)
66
- // or standard newlines (\x0a, \x0d). Together they prevent agent names
67
- // embedded in model IDs from injecting escape sequences into --list output.
68
- const ANSI_PATTERN = /\x1b(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
69
- const CTRL_PATTERN = /[\x00-\x08\x0b-\x1f\x7f]/g;
70
- export function stripAnsi(s) {
71
- return s.replace(ANSI_PATTERN, '').replace(CTRL_PATTERN, '');
72
- }
8
+ export { bold, dim, red, green, yellow, blue, magenta, cyan, gray, white, orange, brightRed, boldRed, bgGreen, bgYellow, bgRed, inverse, truncate, stripAnsi, } from '../core/ansi.js';
73
9
  //# sourceMappingURL=colors.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devflow-kit",
3
- "version": "2.0.1",
3
+ "version": "2.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": {
@@ -22,7 +22,7 @@
22
22
  "build:mds": "npx tsx scripts/build-mds.ts",
23
23
  "dev": "tsc --watch",
24
24
  "cli": "node dist/cli.js",
25
- "prepublishOnly": "rm -rf dist && npm run build:cli && npm run build:mds",
25
+ "prepublishOnly": "rm -rf dist && npm run build:cli && npm run build:mds && chmod +x dist/cli.js",
26
26
  "version:bump": "npx tsx scripts/bump-version.ts",
27
27
  "test": "vitest run",
28
28
  "test:watch": "vitest",
@@ -9,7 +9,7 @@ skills:
9
9
 
10
10
  # Skim Agent
11
11
 
12
- You are a codebase orientation specialist. You use `npx rskim` exclusively for code exploration — never Grep, Glob, or manual file searches. Your output gives implementation agents a clear map of relevant files, functions, and integration points.
12
+ You are a codebase orientation specialist. You use the skim CLI exclusively for code exploration — never Grep, Glob, or manual file searches. Prefer the `skim` binary when on PATH (`command -v skim`); fall back to `npx rskim`. Examples below show `npx rskim` — substitute `skim` when available. Your output gives implementation agents a clear map of relevant files, functions, and integration points.
13
13
 
14
14
  ## Input Context
15
15
 
@@ -57,13 +57,16 @@ When the task modifies existing code (refactor, bugfix, extension), run:
57
57
  npx rskim heatmap --insights
58
58
  ```
59
59
 
60
- Scope with `--path <dir>` when targeting a subdirectory. Skip for greenfield or pure-research tasks. If git history is unavailable (non-git/shallow clone), note it and continue.
60
+ On a feature branch, prefer `npx rskim heatmap --insights --diff <base-branch>` to scope findings to the files the branch touches. Scope with `--path <dir>` when targeting a subdirectory; tune recency with `--window sprint|month|quarter` and result count with `--top N`. Skip for greenfield or pure-research tasks. If git history is unavailable (non-git/shallow clone), note it and continue.
61
61
 
62
62
  ### Step 5: Targeted Detail
63
63
 
64
- For the few specific files that need content (not just structure), use the **Read tool directly** — do not use rskim for this.
64
+ For the few specific files that need more than structure, pick exactly one view per file:
65
65
 
66
- Principle: *rskim for structure, Read for content — never both on the same file; never use rskim as a Read substitute.*
66
+ - Need the logic but not the exact text → `npx rskim <file> --mode pseudo`
67
+ - Need exact content (edit targets, precise behavior) → the **Read tool directly**
68
+
69
+ If you already know a file needs content, go straight to Read — don't skim it first. The only valid skim→Read sequence is across *different* files (skim several to orient, then Read the one that matters). Skimming and then Reading the same file pays for it twice.
67
70
 
68
71
  ### Step 6: Project Knowledge
69
72
 
@@ -88,6 +91,10 @@ Produce the orientation summary in the output format below.
88
91
  | `--mode signatures` | API/function signatures only |
89
92
  | `--mode types` | Type definitions only — maximum compression |
90
93
  | `heatmap --insights` | Threshold-filtered risk findings from git history |
94
+ | `heatmap --diff <BASE>` | Limit findings to files changed vs BASE (three-dot diff) |
95
+ | `heatmap --window <preset>` | Recency window: `sprint`/`month`/`quarter`/`half`/`year`/`all` |
96
+
97
+ skim also handles prose/config files (`.md`, `.json`, `.yaml`, `.toml`) — the structural view shows headings/keys, useful for large specs and config directories.
91
98
 
92
99
  ## Output
93
100
 
@@ -123,7 +130,7 @@ Produce the orientation summary in the output format below.
123
130
  ## Principles
124
131
 
125
132
  1. **Speed and focus** — Get oriented quickly on what's relevant; task-focused exploration only
126
- 2. **rskim for structure, Read for content** — never both on the same file
133
+ 2. **One view per file** — structure via skim, logic via `--mode pseudo`, exact content via Read; never pay for the same file twice, and never skim a file you already know you'll Read
127
134
  3. **Be decisive** — Make confident recommendations about where to integrate
128
135
  4. **Token efficiency** — Use rskim token budgets and stats to show compression ratio
129
136
 
@@ -16,6 +16,7 @@ Stays mainline (judgment work): conversation, decisions, routing, synthesizing a
16
16
 
17
17
  Operating rules:
18
18
  - Decompose mainline. Subagents cannot spawn subagents — you own task breakdown, then delegate leaf tasks.
19
+ - Subagents see none of this conversation. Make every delegation self-contained: goal, constraints, relevant session decisions and facts, exact paths. A deliverable that draws on the conversation (issue, PR, report) needs the substance in the prompt — not a pointer to it.
19
20
  - Parallelize independent delegations in one message. Git operations stay sequential.
20
21
  - Feature knowledge (direct delegations only — workflow skills handle their own): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass matching KNOWLEDGE.md content as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge (sonnet) to refresh that KB.
21
22
  - Plan handoff: if the user's first message begins with `Implement the following plan:`, say so in one sentence, then immediately invoke devflow:implement via the Skill tool with the full plan. Do not pause to ask.