devflow-kit 3.5.0 → 3.7.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 +178 -1
- package/README.md +10 -6
- package/dist/agents/code.md +7 -11
- package/dist/agents/{diagnose.md → debug.md} +4 -4
- package/dist/agents/{code-fix.md → fix.md} +7 -8
- package/dist/agents/{scrutinize.md → harden.md} +6 -6
- package/dist/agents/{code-edit.md → refactor.md} +8 -9
- package/dist/agents/review.md +2 -0
- package/dist/cli/agents-view/render.js +1 -1
- package/dist/cli/commands/gates.js +276 -0
- package/dist/cli/commands/init-seed.js +9 -1
- package/dist/cli/commands/init.js +34 -11
- package/dist/cli/commands/learning.js +12 -12
- package/dist/cli/commands/legacy-hooks.js +1 -1
- package/dist/cli/commands/memory.js +18 -0
- package/dist/cli/commands/uninstall.js +111 -5
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +21 -17
- package/dist/commands/code-review.md +65 -66
- package/dist/commands/debug.md +17 -17
- package/dist/commands/dynamic-build.md +104 -116
- package/dist/commands/dynamic-plan.md +13 -11
- package/dist/commands/dynamic-profile.md +9 -7
- package/dist/commands/dynamic-tickets.md +13 -11
- package/dist/commands/explore.md +15 -15
- package/dist/commands/implement.md +80 -154
- package/dist/commands/plan.md +34 -66
- package/dist/commands/release.md +11 -7
- package/dist/commands/research.md +14 -10
- package/dist/commands/resolve.md +44 -88
- package/dist/commands/self-review.md +44 -44
- package/dist/core/agent-models.js +47 -11
- package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
- package/dist/core/echo-value.js +42 -0
- package/dist/core/evidence-policy.js +17 -5
- package/dist/core/gate-level.js +213 -0
- package/dist/core/learning-queue-cleanup.js +1 -1
- package/dist/core/manifest.js +26 -10
- package/dist/core/migrations.js +63 -44
- package/dist/core/model-discovery.js +7 -1
- package/dist/core/plugins.js +14 -12
- package/dist/core/project-paths.js +3 -3
- package/dist/core/tracker.js +5 -33
- package/dist/hud/codex-usage.js +247 -0
- package/dist/hud/components/codex-quota.js +37 -0
- package/dist/hud/components/usage-quota.js +1 -1
- package/dist/hud/config.js +1 -0
- package/dist/hud/index.js +16 -3
- package/dist/hud/render.js +3 -1
- package/dist/learning-off/agents/code.md +7 -11
- package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
- package/dist/learning-off/agents/{code-fix.md → fix.md} +7 -8
- package/dist/learning-off/agents/{scrutinize.md → harden.md} +6 -6
- package/dist/learning-off/agents/{code-edit.md → refactor.md} +8 -9
- package/dist/learning-off/agents/review.md +2 -0
- package/dist/learning-off/commands/bug-analysis.md +9 -9
- package/dist/learning-off/commands/code-review.md +54 -59
- package/dist/learning-off/commands/debug.md +6 -10
- package/dist/learning-off/commands/dynamic-build.md +98 -112
- package/dist/learning-off/commands/dynamic-plan.md +5 -5
- package/dist/learning-off/commands/dynamic-profile.md +1 -1
- package/dist/learning-off/commands/dynamic-tickets.md +5 -5
- package/dist/learning-off/commands/explore.md +4 -8
- package/dist/learning-off/commands/implement.md +68 -146
- package/dist/learning-off/commands/plan.md +23 -58
- package/dist/learning-off/commands/release.md +4 -2
- package/dist/learning-off/commands/research.md +2 -2
- package/dist/learning-off/commands/resolve.md +33 -81
- package/dist/learning-off/commands/self-review.md +32 -36
- package/dist/skills/git/references/pr/check-merge-readiness.md +2 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +4 -6
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +7 -9
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +7 -15
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +7 -15
- package/dist/targets/claude-code/installer.js +117 -84
- package/dist/targets/claude-code/learning-install.js +15 -3
- package/dist/targets/claude-code/tracker-servers.js +1 -1
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +7 -8
- package/src/assets/agents/code.mds +4 -7
- package/src/assets/agents/{learning.md → curate.md} +8 -10
- package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
- package/src/assets/agents/evaluate.md +4 -4
- package/src/assets/agents/{code-fix.mds → fix.mds} +5 -5
- package/src/assets/agents/{scrutinize.mds → harden.mds} +6 -6
- package/src/assets/agents/{simplify.md → polish.md} +4 -4
- package/src/assets/agents/{code-edit.mds → refactor.mds} +6 -6
- package/src/assets/agents/review.mds +2 -0
- package/src/assets/agents/synthesize.md +2 -2
- package/src/assets/agents/test.md +2 -2
- package/src/assets/commands/_partials/_compliance.mds +1 -1
- package/src/assets/commands/_partials/_decisions.mds +26 -6
- package/src/assets/commands/_partials/_engine.mds +15 -15
- package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
- package/src/assets/commands/_partials/_knowledge.mds +14 -9
- package/src/assets/commands/_partials/_preamble.mds +4 -2
- package/src/assets/commands/_partials/_roster.mds +4 -4
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_wave.mds +10 -17
- package/src/assets/commands/bug-analysis.mds +7 -7
- package/src/assets/commands/code-review.mds +51 -56
- package/src/assets/commands/debug.mds +2 -2
- package/src/assets/commands/dynamic-build.mds +67 -74
- package/src/assets/commands/implement.mds +65 -127
- package/src/assets/commands/plan.mds +21 -61
- package/src/assets/commands/release.mds +5 -1
- package/src/assets/commands/resolve.mds +28 -76
- package/src/assets/commands/self-review.mds +27 -27
- package/src/assets/mds/git/_ops_steps.mds +3 -3
- package/src/assets/mds/git/_pr.mds +6 -6
- package/src/assets/mds/tracker/_common.mds +23 -12
- package/src/assets/mds/tracker/_github.mds +2 -9
- package/src/assets/mds/tracker/_jira.mds +2 -15
- package/src/assets/mds/tracker/_linear.mds +2 -15
- package/src/assets/scripts/ci-wait.cjs +22 -4
- package/src/assets/scripts/claude-md-audit.cjs +66 -51
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +404 -95
- package/src/assets/scripts/hooks/capture-prompt +1 -1
- package/src/assets/scripts/hooks/capture-question +1 -1
- package/src/assets/scripts/hooks/capture-turn +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +13 -2
- package/src/assets/scripts/hooks/json-parse +18 -3
- package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -4
- package/src/assets/scripts/hooks/memory-permission +358 -0
- package/src/assets/scripts/hooks/preamble +1 -1
- package/src/assets/scripts/hooks/session-start-context +60 -47
- package/src/assets/scripts/hooks/session-start-memory +26 -5
- package/src/assets/scripts/lib/project-config.cjs +19 -4
- package/src/assets/scripts/resolve-settings.cjs +200 -32
- package/src/assets/scripts/verify-evidence.cjs +726 -9
- package/src/assets/skills/agent-references/SKILL.md +14 -6
- package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +1 -1
- package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
- package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
- package/src/assets/skills/apply-decisions/SKILL.md +6 -4
- package/src/assets/skills/docs-framework/SKILL.md +3 -3
- package/src/assets/skills/qa/SKILL.md +1 -1
- package/src/assets/skills/quality-gates/SKILL.md +7 -7
- package/src/assets/skills/quality-gates/references/report-template.md +7 -7
- /package/src/assets/skills/agent-references/references/{learning → curate}/maintain.md +0 -0
|
@@ -142,38 +142,56 @@ export function validateWorkerValue(field, value) {
|
|
|
142
142
|
// Key migration
|
|
143
143
|
// ---------------------------------------------------------------------------
|
|
144
144
|
/**
|
|
145
|
-
* Maps
|
|
145
|
+
* Maps every retired agent key to the live slug that replaces it. Two renames feed it:
|
|
146
146
|
*
|
|
147
|
-
*
|
|
148
|
-
* (
|
|
149
|
-
*
|
|
150
|
-
*
|
|
147
|
+
* - The 2.0.0 action-verb rename (coder, reviewer, …) and the 1.x names that rename
|
|
148
|
+
* retired (scrutinizer, simplifier, bug-analyzer).
|
|
149
|
+
* - The rename of six agents to the names that state their job (code-fix, code-edit,
|
|
150
|
+
* scrutinize, simplify, diagnose, learning).
|
|
151
|
+
*
|
|
152
|
+
* SINGLE HOP: every value is a live slug and no value is itself a key. A 1.x name
|
|
153
|
+
* therefore points straight at its final slug (scrutinizer → harden, never
|
|
154
|
+
* scrutinizer → scrutinize → harden), so one pass of canonicaliseAgentKeys() lands
|
|
155
|
+
* every key. The two canonicalise-agent-keys migrations in migrations.ts call it to
|
|
156
|
+
* rewrite ~/.devflow/agent-models.json; readAgentMapping() calls it on every read.
|
|
151
157
|
*
|
|
152
158
|
* PROTOTYPE SAFETY: Object.create(null) prevents prototype-pollution.
|
|
153
159
|
* Every lookup MUST use Object.hasOwn — never key in LEGACY_AGENT_KEYS or
|
|
154
160
|
* direct bracket access. `--set constructor` would otherwise return a function
|
|
155
161
|
* that flows into path.join (the object-literal lookup on argv trap).
|
|
156
162
|
*
|
|
163
|
+
* ORDER: entries are declared oldest first. canonicaliseAgentKeys() reads that order
|
|
164
|
+
* when two keys meet at one slug (scrutinizer and scrutinize both mean harden): the
|
|
165
|
+
* entry declared later is the more recent name and wins.
|
|
166
|
+
*
|
|
157
167
|
* DORMANCY: entries here are stored whether or not the key rename has been
|
|
158
168
|
* applied to the file yet. canonicaliseAgentKeys() is idempotent, so
|
|
159
169
|
* double-execution from concurrent sessions is a harmless no-op — the second
|
|
160
170
|
* run finds the file already migrated and makes zero writes.
|
|
161
171
|
*/
|
|
162
172
|
export const LEGACY_AGENT_KEYS = Object.assign(Object.create(null), {
|
|
163
|
-
//
|
|
173
|
+
// 1.x names (noun form), retargeted to the live slug of their final rename.
|
|
174
|
+
'scrutinizer': 'harden',
|
|
175
|
+
'simplifier': 'polish',
|
|
176
|
+
'bug-analyzer': 'debug',
|
|
177
|
+
// 2.0.0 action-verb rename (old slug → new slug).
|
|
164
178
|
'coder': 'code',
|
|
165
179
|
'designer': 'design',
|
|
166
180
|
'evaluator': 'evaluate',
|
|
167
181
|
'researcher': 'research',
|
|
168
182
|
'reviewer': 'review',
|
|
169
|
-
'scrutinizer': 'scrutinize',
|
|
170
|
-
'simplifier': 'simplify',
|
|
171
183
|
'skimmer': 'skim',
|
|
172
184
|
'synthesizer': 'synthesize',
|
|
173
185
|
'tester': 'test',
|
|
174
186
|
'triager': 'triage',
|
|
175
187
|
'validator': 'validate',
|
|
176
|
-
|
|
188
|
+
// Rename of six agents to the names that state their job.
|
|
189
|
+
'code-fix': 'fix',
|
|
190
|
+
'code-edit': 'refactor',
|
|
191
|
+
'scrutinize': 'harden',
|
|
192
|
+
'simplify': 'polish',
|
|
193
|
+
'diagnose': 'debug',
|
|
194
|
+
'learning': 'curate',
|
|
177
195
|
});
|
|
178
196
|
/**
|
|
179
197
|
* Canonicalise agent keys in a raw agents map by applying LEGACY_AGENT_KEYS.
|
|
@@ -184,6 +202,10 @@ export const LEGACY_AGENT_KEYS = Object.assign(Object.create(null), {
|
|
|
184
202
|
*
|
|
185
203
|
* Collision detection is against the ORIGINAL map so results are order-independent.
|
|
186
204
|
*
|
|
205
|
+
* Two legacy keys that name one agent (scrutinizer and scrutinize both mean harden) meet
|
|
206
|
+
* at one slug. The key declared later in LEGACY_AGENT_KEYS, the more recent name, wins;
|
|
207
|
+
* the other is dropped. The pick reads the declaration order, never the file order.
|
|
208
|
+
*
|
|
187
209
|
* The function is pure and idempotent: calling it twice on the same input
|
|
188
210
|
* produces the same result as calling it once (idempotent under concurrent execution).
|
|
189
211
|
*
|
|
@@ -208,6 +230,19 @@ export function canonicaliseAgentKeys(rawAgents, onWarning) {
|
|
|
208
230
|
}
|
|
209
231
|
// Snapshot the original keys for collision detection (order-independent result).
|
|
210
232
|
const originalKeys = new Set(Object.keys(rawAgents));
|
|
233
|
+
// When several legacy keys in the file name one agent, the one declared latest in
|
|
234
|
+
// LEGACY_AGENT_KEYS is the one that is renamed; the others are dropped below.
|
|
235
|
+
const declared = Object.keys(LEGACY_AGENT_KEYS);
|
|
236
|
+
const winnerByTarget = new Map();
|
|
237
|
+
for (const oldKey of originalKeys) {
|
|
238
|
+
if (oldKey === '__proto__' || !Object.hasOwn(LEGACY_AGENT_KEYS, oldKey))
|
|
239
|
+
continue;
|
|
240
|
+
const target = LEGACY_AGENT_KEYS[oldKey];
|
|
241
|
+
const held = winnerByTarget.get(target);
|
|
242
|
+
if (held === undefined || declared.indexOf(oldKey) > declared.indexOf(held)) {
|
|
243
|
+
winnerByTarget.set(target, oldKey);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
211
246
|
const result = { ...rawAgents };
|
|
212
247
|
let didMutate = false;
|
|
213
248
|
for (const oldKey of originalKeys) {
|
|
@@ -237,8 +272,9 @@ export function canonicaliseAgentKeys(rawAgents, onWarning) {
|
|
|
237
272
|
didMutate = true;
|
|
238
273
|
continue;
|
|
239
274
|
}
|
|
240
|
-
if (originalKeys.has(newKey)) {
|
|
241
|
-
// Collision: new (canonical) key already present
|
|
275
|
+
if (originalKeys.has(newKey) || winnerByTarget.get(newKey) !== oldKey) {
|
|
276
|
+
// Collision: new (canonical) key already present, or a more recent legacy key
|
|
277
|
+
// names the same agent → that value wins, old dropped.
|
|
242
278
|
// Warning is surfaced by the caller's structured dropped-block (e.g. migrations.ts)
|
|
243
279
|
// to avoid printing the same collision twice in two different vocabularies (F8).
|
|
244
280
|
dropped.push(oldKey);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getDevFlowDirectory } from '../targets/claude-code/claude-paths.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getCurateTuningConfigPath } from './project-paths.js';
|
|
5
5
|
const VALID_MODELS = new Set(['opus', 'sonnet', 'haiku']);
|
|
6
6
|
function isValidModel(value) {
|
|
7
7
|
return typeof value === 'string' && VALID_MODELS.has(value);
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* D-LEARNING-SHIPPED-ROW: the default model is the model the
|
|
11
|
-
* with, the `model:` line of src/assets/agents/
|
|
10
|
+
* D-LEARNING-SHIPPED-ROW: the default model is the model the Curate agent ships
|
|
11
|
+
* with, the `model:` line of src/assets/agents/curate.md. With no layer
|
|
12
12
|
* configuring a model, the spawn directive names none and that line decides, so a
|
|
13
13
|
* different default here would describe a run that never happens.
|
|
14
|
-
* tests/decisions/config.test.ts holds this value equal to the
|
|
14
|
+
* tests/decisions/config.test.ts holds this value equal to the Curate row of the
|
|
15
15
|
* agent-config table, which a change of the shipped tier edits with the frontmatter.
|
|
16
16
|
*/
|
|
17
17
|
const DEFAULTS = {
|
|
@@ -19,12 +19,12 @@ const DEFAULTS = {
|
|
|
19
19
|
debug: false,
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Apply a single JSON config layer onto a
|
|
22
|
+
* Apply a single JSON config layer onto a CurateTuningConfig, returning a new object.
|
|
23
23
|
* Skips fields with wrong types. Swallows parse errors — callers see defaults.
|
|
24
24
|
* Unknown fields (e.g. an old config still on disk with extra knobs) are
|
|
25
25
|
* silently ignored, not an error.
|
|
26
26
|
*/
|
|
27
|
-
export function
|
|
27
|
+
export function applyCurateTuningConfigLayer(config, json) {
|
|
28
28
|
try {
|
|
29
29
|
const parsed = JSON.parse(json);
|
|
30
30
|
// Object-shape guard mirrors coerceConfig in feature-config.ts: a JSON
|
|
@@ -34,7 +34,7 @@ export function applyLearningTuningConfigLayer(config, json) {
|
|
|
34
34
|
}
|
|
35
35
|
const raw = parsed;
|
|
36
36
|
return {
|
|
37
|
-
// Only accept a model value that belongs to the closed
|
|
37
|
+
// Only accept a model value that belongs to the closed CurateModelAlias domain.
|
|
38
38
|
model: isValidModel(raw.model) ? raw.model : config.model,
|
|
39
39
|
debug: typeof raw.debug === 'boolean' ? raw.debug : config.debug,
|
|
40
40
|
};
|
|
@@ -54,13 +54,13 @@ function readConfigFile(filePath) {
|
|
|
54
54
|
catch (err) {
|
|
55
55
|
if (err.code !== 'ENOENT') {
|
|
56
56
|
// Warn but don't crash — callers fall back to defaults.
|
|
57
|
-
console.warn(`[
|
|
57
|
+
console.warn(`[curate-tuning-config] warning: could not read ${filePath}: ${err.message}`);
|
|
58
58
|
}
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Load and merge
|
|
63
|
+
* Load and merge curate agent tuning configuration.
|
|
64
64
|
*
|
|
65
65
|
* Priority (highest wins): project config → global config → defaults.
|
|
66
66
|
*
|
|
@@ -71,18 +71,18 @@ function readConfigFile(filePath) {
|
|
|
71
71
|
*
|
|
72
72
|
* Invalid JSON in either file is silently ignored and treated as absent.
|
|
73
73
|
*/
|
|
74
|
-
export function
|
|
74
|
+
export function loadCurateTuningConfig(ledgerRoot) {
|
|
75
75
|
const globalConfigPath = path.join(getDevFlowDirectory(), 'learning.json');
|
|
76
|
-
const projectConfigPath =
|
|
76
|
+
const projectConfigPath = getCurateTuningConfigPath(ledgerRoot);
|
|
77
77
|
let config = { ...DEFAULTS };
|
|
78
78
|
const globalJson = readConfigFile(globalConfigPath);
|
|
79
79
|
if (globalJson !== null) {
|
|
80
|
-
config =
|
|
80
|
+
config = applyCurateTuningConfigLayer(config, globalJson);
|
|
81
81
|
}
|
|
82
82
|
const projectJson = readConfigFile(projectConfigPath);
|
|
83
83
|
if (projectJson !== null) {
|
|
84
|
-
config =
|
|
84
|
+
config = applyCurateTuningConfigLayer(config, projectJson);
|
|
85
85
|
}
|
|
86
86
|
return config;
|
|
87
87
|
}
|
|
88
|
-
//# sourceMappingURL=
|
|
88
|
+
//# sourceMappingURL=curate-tuning-config.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render an untrusted value for a terminal message.
|
|
3
|
+
*
|
|
4
|
+
* A neutral home for the sanitizer that every parser echoing a rejected value
|
|
5
|
+
* shares (the tracker provider and the gate level today), so no parser imports
|
|
6
|
+
* another feature's module just to reach it.
|
|
7
|
+
*/
|
|
8
|
+
/** Longest rejected value echoed back to the terminal, in CODE POINTS. */
|
|
9
|
+
const MAX_ECHOED_VALUE = 40;
|
|
10
|
+
/**
|
|
11
|
+
* Render an untrusted token for a terminal message.
|
|
12
|
+
*
|
|
13
|
+
* Rejected values are echoed so the user can see what they typed, so they are
|
|
14
|
+
* third-party input at a display sink: control characters (terminal escapes,
|
|
15
|
+
* BEL, newlines) are replaced and the value is truncated.
|
|
16
|
+
*
|
|
17
|
+
* Takes `unknown`, and a non-string renders as its TYPE: the never-throws
|
|
18
|
+
* contract of the callers has to hold for what reaches this sink, not
|
|
19
|
+
* only for what the signature says — a caller reads hand-editable
|
|
20
|
+
* files, and a caller-side guard is one edit from being gone. Naming the type
|
|
21
|
+
* also keeps the render total, where `String()` would hand control to a
|
|
22
|
+
* caller-supplied `toString` — itself both a throw path and an echo path this
|
|
23
|
+
* function exists to close.
|
|
24
|
+
*/
|
|
25
|
+
export function describeEchoedValue(raw) {
|
|
26
|
+
if (typeof raw !== 'string')
|
|
27
|
+
return `<${raw === null ? 'null' : typeof raw}>`;
|
|
28
|
+
// The class is written with ESCAPES, never literal control bytes. A raw NUL
|
|
29
|
+
// makes grep classify this whole file as binary — it prints "Binary file
|
|
30
|
+
// matches" and skips the lines — so every grep-based sweep over src/core/
|
|
31
|
+
// silently stops covering the file while still exiting 0. Pinned by
|
|
32
|
+
// tests/guards/no-control-bytes.test.ts.
|
|
33
|
+
// eslint-disable-next-line no-control-regex
|
|
34
|
+
const stripped = raw.replace(/[\x00-\x1f\x7f]/g, '?');
|
|
35
|
+
// Cut by CODE POINT. `slice` cuts UTF-16 code units, so a cut landing inside an
|
|
36
|
+
// astral character (emoji, CJK ext-B) emits the lone surrogate half of it.
|
|
37
|
+
const points = [...stripped];
|
|
38
|
+
return points.length > MAX_ECHOED_VALUE
|
|
39
|
+
? `${points.slice(0, MAX_ECHOED_VALUE).join('')}…`
|
|
40
|
+
: stripped;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=echo-value.js.map
|
|
@@ -331,15 +331,27 @@ export function repoComplianceStatusLines(loaded, opts) {
|
|
|
331
331
|
* tracked by git, or null (D-PERSONAL-UNTRACKED). The resolver ignores such a file
|
|
332
332
|
* and says so on stderr, but prompts run it with stderr discarded, so a status
|
|
333
333
|
* command is where the user sees why their personal settings have no effect.
|
|
334
|
+
*
|
|
335
|
+
* Takes the ONE resolved result, so a caller that already holds it adds no second
|
|
336
|
+
* resolve (each resolve forks git).
|
|
337
|
+
*/
|
|
338
|
+
export function trackedConfigWarning(settings) {
|
|
339
|
+
if (!settings.personalTracked)
|
|
340
|
+
return null;
|
|
341
|
+
const file = settingsSourceFile('personal');
|
|
342
|
+
return `Git tracks an entry at or under ${file}, so devflow ignores that file. It holds personal settings. ` +
|
|
343
|
+
'Find the entry in the output of: git ls-files. Untrack it with: git rm --cached <entry>. ' +
|
|
344
|
+
'Then check the file contents, or delete the file. ' +
|
|
345
|
+
'Untracking leaves the committed bytes on disk as your personal file.';
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* {@link trackedConfigWarning} for a caller that holds the loaded module and not a
|
|
349
|
+
* resolved result: it resolves once and returns null when the module is unusable.
|
|
334
350
|
*/
|
|
335
351
|
export function personalConfigTrackedWarning(loaded, opts) {
|
|
336
352
|
if (!loaded.ok)
|
|
337
353
|
return null;
|
|
338
|
-
|
|
339
|
-
return null;
|
|
340
|
-
const file = settingsSourceFile('personal');
|
|
341
|
-
return `${file} is tracked by git, so devflow ignores it — it holds personal settings. ` +
|
|
342
|
-
`Untrack it with: git rm --cached ${file}`;
|
|
354
|
+
return trackedConfigWarning(loaded.value.resolveSettings(opts));
|
|
343
355
|
}
|
|
344
356
|
/** The policies the hint maps, in the order it prints them. */
|
|
345
357
|
const HINT_POLICIES = ['standard', 'required'];
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { promises as fs, constants as fsConstants } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
4
|
+
import { hasRequiredManifestFields } from './manifest.js';
|
|
5
|
+
import { describeEchoedValue } from './echo-value.js';
|
|
6
|
+
/**
|
|
7
|
+
* The gate level: how much checking a command applies. One of three words, from
|
|
8
|
+
* most to least checking.
|
|
9
|
+
*
|
|
10
|
+
* `features.gateLevel` in `~/.devflow/manifest.json` is the MACHINE default, and
|
|
11
|
+
* `gateLevel` in a repository's personal `.devflow/config.json` overrides it.
|
|
12
|
+
* Neither is a feature switch: a switch can only be narrowed by a repository
|
|
13
|
+
* layer, but a level may be raised or lowered by the personal file. The settings
|
|
14
|
+
* resolver (`resolve-settings.cjs`) folds both into the `GATE_LEVEL=` field of its
|
|
15
|
+
* settings line; this module owns the closed set, the parser and the machine
|
|
16
|
+
* layer's raw reader and writer. No code here chooses a level from a task or a
|
|
17
|
+
* diff.
|
|
18
|
+
*
|
|
19
|
+
* A present value that is not one of the three words is MALFORMED. A reader reads
|
|
20
|
+
* it as `full` and leaves it on disk as written: no read and no unrelated write
|
|
21
|
+
* heals it to absent, which would lower the level without a word.
|
|
22
|
+
*/
|
|
23
|
+
export const GATE_LEVELS = Object.freeze(['full', 'standard', 'lean']);
|
|
24
|
+
/** The level when neither the personal file nor the manifest records one. */
|
|
25
|
+
export const DEFAULT_GATE_LEVEL = 'standard';
|
|
26
|
+
/**
|
|
27
|
+
* Largest manifest read, in bytes. Pinned by a parity test to the resolver
|
|
28
|
+
* library's MAX_MANIFEST_BYTES, so the CLI and a prompt never disagree about a
|
|
29
|
+
* manifest that is too large to read.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_MANIFEST_BYTES = 1048576;
|
|
32
|
+
const VALID_LEVELS_LIST = GATE_LEVELS.join(', ');
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function isGateLevel(value) {
|
|
37
|
+
return typeof value === 'string' && GATE_LEVELS.includes(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Strict boundary parser for a gate level typed on the command line.
|
|
41
|
+
*
|
|
42
|
+
* REJECT, NEVER REPAIR: membership is byte-exact. No trim, no case folding, no
|
|
43
|
+
* alias. `Lean` and `lean ` are errors, not `lean`. The message names every valid
|
|
44
|
+
* level and echoes the offending value only in the sanitized form of
|
|
45
|
+
* {@link describeEchoedValue}.
|
|
46
|
+
*/
|
|
47
|
+
export function parseGateLevel(value) {
|
|
48
|
+
if (isGateLevel(value))
|
|
49
|
+
return { ok: true, value };
|
|
50
|
+
if (value === undefined || value === '') {
|
|
51
|
+
return { ok: false, error: `Missing gate level. Valid levels: ${VALID_LEVELS_LIST}` };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: `Unknown gate level: "${describeEchoedValue(value)}". Valid levels: ${VALID_LEVELS_LIST}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The machine layer's level in a RAW parsed manifest. Pure and total.
|
|
60
|
+
*
|
|
61
|
+
* null no level is recorded: not an object, `features` not an object, or
|
|
62
|
+
* no own `gateLevel` key
|
|
63
|
+
* 'invalid' a `gateLevel` key is present and is not one of the three words
|
|
64
|
+
* a level the recorded level
|
|
65
|
+
*
|
|
66
|
+
* Deliberately NOT built on readManifest(): that reader rejects a manifest
|
|
67
|
+
* missing a required field and heal-writes, and a read-only status must do
|
|
68
|
+
* neither. The resolver script's `machineGateLevel` is the CommonJS twin; a
|
|
69
|
+
* table test holds the two to the same rows.
|
|
70
|
+
*/
|
|
71
|
+
export function machineGateLevel(raw) {
|
|
72
|
+
if (!isPlainObject(raw))
|
|
73
|
+
return null;
|
|
74
|
+
const features = raw.features;
|
|
75
|
+
if (!isPlainObject(features))
|
|
76
|
+
return null;
|
|
77
|
+
if (!Object.hasOwn(features, 'gateLevel'))
|
|
78
|
+
return null;
|
|
79
|
+
const value = features.gateLevel;
|
|
80
|
+
return isGateLevel(value) ? value : 'invalid';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Set `features.gateLevel` in a RAW parsed manifest. Pure — returns a new object
|
|
84
|
+
* and never mutates its input. Null when the top level or `features` is not a
|
|
85
|
+
* plain object (there is no `features` record to write into).
|
|
86
|
+
*
|
|
87
|
+
* Only `features.gateLevel` and `updatedAt` change; every other key is carried
|
|
88
|
+
* verbatim, including keys this build does not model.
|
|
89
|
+
*/
|
|
90
|
+
export function setMachineGateLevel(raw, level, now) {
|
|
91
|
+
if (!isPlainObject(raw) || !isPlainObject(raw.features))
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
...raw,
|
|
95
|
+
features: { ...raw.features, gateLevel: level },
|
|
96
|
+
updatedAt: now,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const ABSENT = Object.freeze({ kind: 'absent' });
|
|
100
|
+
const CORRUPT = Object.freeze({ kind: 'corrupt' });
|
|
101
|
+
function startsWithBom(bytes) {
|
|
102
|
+
return bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
|
|
103
|
+
}
|
|
104
|
+
/** Bytes of the manifest, or why not: one bounded read of a regular file, followed through links. */
|
|
105
|
+
async function readManifestBytes(manifestPath) {
|
|
106
|
+
try {
|
|
107
|
+
let st;
|
|
108
|
+
try {
|
|
109
|
+
st = await fs.stat(manifestPath);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const code = err.code;
|
|
113
|
+
return code === 'ENOENT' || code === 'ENOTDIR' ? 'absent' : 'corrupt';
|
|
114
|
+
}
|
|
115
|
+
if (!st.isFile() || st.size > MAX_MANIFEST_BYTES)
|
|
116
|
+
return 'corrupt';
|
|
117
|
+
// O_NONBLOCK: a FIFO swapped in after the stat cannot block the open.
|
|
118
|
+
const handle = await fs.open(manifestPath, fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK || 0));
|
|
119
|
+
try {
|
|
120
|
+
const opened = await handle.stat();
|
|
121
|
+
if (!opened.isFile() || opened.size > MAX_MANIFEST_BYTES)
|
|
122
|
+
return 'corrupt';
|
|
123
|
+
// One byte past the stat'd size: a file that grew between the stat and the
|
|
124
|
+
// read is refused rather than truncated into something that parses. Each
|
|
125
|
+
// pass reads at least one byte or stops, so the loop is bounded by the buffer.
|
|
126
|
+
const buf = Buffer.alloc(opened.size + 1);
|
|
127
|
+
let total = 0;
|
|
128
|
+
for (let pass = 0; pass < buf.length && total < buf.length; pass++) {
|
|
129
|
+
const { bytesRead } = await handle.read(buf, total, buf.length - total, null);
|
|
130
|
+
if (bytesRead === 0)
|
|
131
|
+
break;
|
|
132
|
+
total += bytesRead;
|
|
133
|
+
}
|
|
134
|
+
return total > opened.size ? 'corrupt' : buf.subarray(0, total);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await handle.close().catch(() => undefined);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return 'corrupt';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Read `<devflowDir>/manifest.json` raw, for the gate level. Never throws, never
|
|
146
|
+
* exits and never writes: unlike readManifest() it heals nothing, models no key
|
|
147
|
+
* and keeps every key it does not name.
|
|
148
|
+
*
|
|
149
|
+
* It follows the resolver library's rules, so the CLI and a prompt see the same
|
|
150
|
+
* file: the path is followed through links, a missing path is absent, and every
|
|
151
|
+
* other failure is corrupt. Bytes decode as UTF-8 with a fatal error, which also
|
|
152
|
+
* strips a leading BOM, and then parse as JSON, so the level is read past a BOM
|
|
153
|
+
* as it is in the resolver. A file with a BOM is still `rejected`, because
|
|
154
|
+
* readManifest() parses the text as read and refuses it.
|
|
155
|
+
*/
|
|
156
|
+
export async function readGateManifest(devflowDir) {
|
|
157
|
+
const bytes = await readManifestBytes(path.join(devflowDir, 'manifest.json'));
|
|
158
|
+
if (bytes === 'absent')
|
|
159
|
+
return ABSENT;
|
|
160
|
+
if (bytes === 'corrupt')
|
|
161
|
+
return CORRUPT;
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return CORRUPT;
|
|
168
|
+
}
|
|
169
|
+
if (!isPlainObject(parsed) || !isPlainObject(parsed.features))
|
|
170
|
+
return CORRUPT;
|
|
171
|
+
const readable = !startsWithBom(bytes) && hasRequiredManifestFields(parsed);
|
|
172
|
+
return { kind: readable ? 'object' : 'rejected', raw: parsed };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Write the machine default to `<devflowDir>/manifest.json` (atomic temp +
|
|
176
|
+
* rename). Never throws. Refuses with `invalid-level`, before any I/O, a level
|
|
177
|
+
* that is not one of the three words, because the type is no check at run time.
|
|
178
|
+
* Refuses with `not-installed` when there is no manifest to write into — a
|
|
179
|
+
* manifest is created by `devflow init`, never by this write.
|
|
180
|
+
*
|
|
181
|
+
* Only `features.gateLevel` and `updatedAt` change. A write to the level the
|
|
182
|
+
* manifest already records changes nothing, `updatedAt` included, because that
|
|
183
|
+
* field means "the manifest's content last changed". A manifest that
|
|
184
|
+
* readManifest() would reject for another key gets the same write as one it
|
|
185
|
+
* accepts: its level is valid, and the fault is not this write's to fix.
|
|
186
|
+
*
|
|
187
|
+
* `now` is injected so a test fixes the timestamp.
|
|
188
|
+
*/
|
|
189
|
+
export async function writeMachineGateLevel(devflowDir, level, now) {
|
|
190
|
+
if (!isGateLevel(level))
|
|
191
|
+
return { kind: 'invalid-level' };
|
|
192
|
+
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
193
|
+
const read = await readGateManifest(devflowDir);
|
|
194
|
+
if (read.kind === 'absent')
|
|
195
|
+
return { kind: 'not-installed' };
|
|
196
|
+
if (read.kind === 'corrupt')
|
|
197
|
+
return { kind: 'unreadable', path: manifestPath };
|
|
198
|
+
const features = read.raw.features;
|
|
199
|
+
if (isPlainObject(features) && features.gateLevel === level)
|
|
200
|
+
return { kind: 'unchanged' };
|
|
201
|
+
try {
|
|
202
|
+
const next = setMachineGateLevel(read.raw, level, now());
|
|
203
|
+
if (next === null)
|
|
204
|
+
return { kind: 'unreadable', path: manifestPath };
|
|
205
|
+
await writeFileAtomicExclusive(manifestPath, JSON.stringify(next, null, 2) + '\n');
|
|
206
|
+
return { kind: 'written' };
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
const code = err?.code;
|
|
210
|
+
return { kind: 'write-failed', path: manifestPath, reason: typeof code === 'string' ? code : 'unknown error' };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=gate-level.js.map
|
|
@@ -11,7 +11,7 @@ import { drainQueueFiles } from './queue-drain.js';
|
|
|
11
11
|
/**
|
|
12
12
|
* Drain the learning (decisions-detection) pending-turns queue, its claimed
|
|
13
13
|
* batch and the claim's owner file so stale turns don't process later — used by
|
|
14
|
-
* both `--clear` and `--disable`. A mid-run
|
|
14
|
+
* both `--clear` and `--disable`. A mid-run Curate agent whose claimed batch
|
|
15
15
|
* vanishes aborts without changes, which is the desired outcome in both cases.
|
|
16
16
|
* Refused, deleting nothing, when `.devflow` or `.devflow/learning` under `gitRoot`
|
|
17
17
|
* is a symbolic link (D-CLI-NO-SYMLINK). ENOENT-tolerant; other errors propagate.
|
package/dist/core/manifest.js
CHANGED
|
@@ -51,6 +51,27 @@ function parseManifestFlags(features, knownFlags) {
|
|
|
51
51
|
// Case C: missing/malformed → empty record
|
|
52
52
|
return { flags: {}, legacy: false };
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a parsed manifest object carries every field {@link readManifest}
|
|
56
|
+
* requires: a version, a plugin list, a `features` object with boolean `ambient`
|
|
57
|
+
* and `memory`, and string `installedAt` and `updatedAt`. Pure. A manifest that
|
|
58
|
+
* fails it reads as "no prior install".
|
|
59
|
+
*
|
|
60
|
+
* Exported so a raw reader can tell a manifest this reader rejects from one it
|
|
61
|
+
* accepts without calling it: readManifest heal-writes, and a read-only status
|
|
62
|
+
* must not.
|
|
63
|
+
*/
|
|
64
|
+
export function hasRequiredManifestFields(data) {
|
|
65
|
+
const features = data.features;
|
|
66
|
+
return !(!data.version ||
|
|
67
|
+
!Array.isArray(data.plugins) ||
|
|
68
|
+
typeof features !== 'object' ||
|
|
69
|
+
features === null ||
|
|
70
|
+
typeof features.ambient !== 'boolean' ||
|
|
71
|
+
typeof features.memory !== 'boolean' ||
|
|
72
|
+
typeof data.installedAt !== 'string' ||
|
|
73
|
+
typeof data.updatedAt !== 'string');
|
|
74
|
+
}
|
|
54
75
|
/**
|
|
55
76
|
* Read and parse the manifest file. Returns null if missing or corrupt.
|
|
56
77
|
*
|
|
@@ -75,17 +96,9 @@ export async function readManifest(devflowDir, opts) {
|
|
|
75
96
|
try {
|
|
76
97
|
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
77
98
|
const data = JSON.parse(content);
|
|
78
|
-
|
|
79
|
-
if (!data.version ||
|
|
80
|
-
!Array.isArray(data.plugins) ||
|
|
81
|
-
typeof features !== 'object' ||
|
|
82
|
-
features === null ||
|
|
83
|
-
typeof features.ambient !== 'boolean' ||
|
|
84
|
-
typeof features.memory !== 'boolean' ||
|
|
85
|
-
typeof data.installedAt !== 'string' ||
|
|
86
|
-
typeof data.updatedAt !== 'string') {
|
|
99
|
+
if (!hasRequiredManifestFields(data))
|
|
87
100
|
return null;
|
|
88
|
-
|
|
101
|
+
const features = data.features;
|
|
89
102
|
// D-FEATURES-ABSENT-ON (a sub-decision of D-FEATURES-NARROW-ONLY,
|
|
90
103
|
// src/core/feature-switch.ts): knowledge and learning are machine-wide
|
|
91
104
|
// switches, and every runtime gate — queue_read_gates in the hooks,
|
|
@@ -172,6 +185,9 @@ export async function readManifest(devflowDir, opts) {
|
|
|
172
185
|
// self-heal behaviour, and a different condition from a per-repo config
|
|
173
186
|
// value outside the domain (which does emit `unknown tracker provider`).
|
|
174
187
|
tracker: normalizeTrackerFeature(features.tracker, opts?.onWarning),
|
|
188
|
+
// The machine gate level, raw (see the field's doc comment): kept when the
|
|
189
|
+
// key is present, a stored `null` included, and left out when it is absent.
|
|
190
|
+
...(Object.hasOwn(features, 'gateLevel') ? { gateLevel: features.gateLevel } : {}),
|
|
175
191
|
},
|
|
176
192
|
installedAt: data.installedAt,
|
|
177
193
|
updatedAt: data.updatedAt,
|