devflow-kit 2.0.1 → 2.1.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 +22 -0
- package/dist/cli/agents-view/render.js +9 -42
- package/dist/cli/agents-view/terminal.js +29 -153
- package/dist/cli/commands/agents.js +11 -1
- package/dist/cli/commands/flags.js +501 -82
- package/dist/cli/commands/init-seed.js +60 -42
- package/dist/cli/commands/init.js +36 -79
- package/dist/cli/commands/proxy.js +42 -13
- package/dist/cli/commands/uninstall.js +2 -3
- package/dist/cli/flags-view/index.js +9 -0
- package/dist/cli/flags-view/render.js +271 -0
- package/dist/cli/flags-view/state.js +478 -0
- package/dist/cli/flags-view/terminal.js +63 -0
- package/dist/cli/tui/cells.js +47 -0
- package/dist/cli/tui/terminal.js +329 -0
- package/dist/cli.js +3 -2
- package/dist/core/ansi.js +84 -0
- package/dist/core/flags.js +938 -123
- package/dist/core/manifest.js +97 -13
- package/dist/core/teammate-mode-cleanup.js +2 -2
- package/dist/hud/colors.js +6 -70
- package/package.json +2 -2
package/dist/core/manifest.js
CHANGED
|
@@ -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:
|
|
56
|
-
knownFlags
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
*
|
package/dist/hud/colors.js
CHANGED
|
@@ -1,73 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ANSI color helpers —
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
|
3
|
+
"version": "2.1.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",
|