devflow-kit 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +71 -17
- package/dist/cli/agents-view/state.js +42 -16
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +142 -51
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +162 -85
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +302 -136
- package/dist/cli/commands/memory.js +36 -15
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +183 -59
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +51 -47
- package/dist/commands/dynamic-plan.md +27 -7
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/explore.md +9 -3
- package/dist/commands/implement.md +20 -16
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +23 -3
- package/dist/commands/research.md +9 -3
- package/dist/commands/resolve.md +9 -12
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +28 -3
- package/dist/core/agent-models.js +204 -42
- package/dist/core/agent-state.js +28 -6
- package/dist/core/ansi.js +2 -2
- package/dist/core/assets.js +1 -1
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +71 -13
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +49 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -81
- package/dist/core/learning-store.js +61 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +13 -8
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/queue-drain.js +31 -0
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +54 -22
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/targets/claude-code/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +59 -32
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +135 -45
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +15 -21
- package/src/assets/agents/design.md +4 -2
- package/src/assets/agents/diagnose.md +3 -1
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +5 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +3 -1
- package/src/assets/agents/review.md +5 -3
- package/src/assets/agents/scrutinize.md +5 -1
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +4 -2
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +11 -9
- package/src/assets/agents/validate.md +14 -10
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +16 -32
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/code-review.mds +0 -2
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +18 -12
- package/src/assets/commands/dynamic-plan.mds +10 -4
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +19 -13
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +23 -3
- package/src/assets/commands/research.mds +9 -3
- package/src/assets/commands/resolve.mds +9 -10
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +3 -3
- package/src/assets/mds/tracker/_jira.mds +3 -3
- package/src/assets/mds/tracker/_linear.mds +3 -3
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
- package/src/assets/scripts/hooks/background-memory-update +97 -33
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +5 -20
- package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +345 -944
- package/src/assets/scripts/hooks/json-parse +25 -129
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +55 -23
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +146 -45
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/src/assets/skills/test-driven-development/SKILL.md +6 -4
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's view of the learning store — a typed seam onto the package's own
|
|
3
|
+
* `hooks/lib/learning-store.cjs`, never a second implementation of it.
|
|
4
|
+
*
|
|
5
|
+
* D-LEARNING-STORE-SEAM: `devflow learning` reads and writes the learning tree only
|
|
6
|
+
* through the store module json-helper's ops and the hooks run, loaded with the
|
|
7
|
+
* evidence-policy seam's `loadScript` from the package's own scripts directory and
|
|
8
|
+
* shape-checked against LEARNING_STORE_SURFACE; the interfaces below are
|
|
9
|
+
* TRANSCRIBED from the store's JSDoc and are this side's only shape authority.
|
|
10
|
+
* Reason: the CLI's own TypeScript reader judged every row by the v1 shape, so it
|
|
11
|
+
* counted v2 observations as invalid, and its `--clear` truncated the log without
|
|
12
|
+
* the lock every learning writer takes.
|
|
13
|
+
*
|
|
14
|
+
* As with the evidence resolver (D-POLICY-CJS-SEAM), the CLI loads the package
|
|
15
|
+
* copy, which `npm` installs with this CLI, not `~/.devflow/scripts`, which
|
|
16
|
+
* `devflow init` refreshes: the two can differ until the next init.
|
|
17
|
+
*/
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { scriptsDir } from './assets.js';
|
|
20
|
+
import { loadScript } from './evidence-policy.js';
|
|
21
|
+
/** The store, relative to src/assets/scripts/ (and ~/.devflow/scripts/). */
|
|
22
|
+
export const LEARNING_STORE_SCRIPT_NAME = join('hooks', 'lib', 'learning-store.cjs');
|
|
23
|
+
/**
|
|
24
|
+
* Every key of LearningStoreModule and the runtime kind the loader requires of
|
|
25
|
+
* it. `satisfies` makes the compiler reject an interface key missing here.
|
|
26
|
+
*/
|
|
27
|
+
export const LEARNING_STORE_SURFACE = Object.freeze({
|
|
28
|
+
INACTIVE_STATUSES: 'string-array',
|
|
29
|
+
ANCHOR_ID_RE: 'regexp',
|
|
30
|
+
readLearningState: 'function',
|
|
31
|
+
buildListing: 'function',
|
|
32
|
+
readListing: 'function',
|
|
33
|
+
formatListing: 'function',
|
|
34
|
+
showByKey: 'function',
|
|
35
|
+
restoreAnchor: 'function',
|
|
36
|
+
clearUnreferenced: 'function',
|
|
37
|
+
resetLearning: 'function',
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Load the learning store from `dir` (default: the package's own scripts
|
|
41
|
+
* directory) and shape-check its surface. Never throws: a missing file is
|
|
42
|
+
* `not-found`; a module that throws on load or lacks a surface key is `unusable`.
|
|
43
|
+
*/
|
|
44
|
+
export function loadLearningStore(dir = scriptsDir()) {
|
|
45
|
+
return loadScript(join(dir, LEARNING_STORE_SCRIPT_NAME), LEARNING_STORE_SURFACE);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Why the store cannot be used, and the remedy: a package reinstall, since the
|
|
49
|
+
* CLI loads the package's own copy, which `devflow init` does not restore.
|
|
50
|
+
*/
|
|
51
|
+
export function formatLearningStoreUnavailable(error) {
|
|
52
|
+
switch (error.kind) {
|
|
53
|
+
case 'not-found': return 'learning store not found — reinstall devflow-kit';
|
|
54
|
+
case 'unusable': return 'learning store failed to load — reinstall devflow-kit';
|
|
55
|
+
default: {
|
|
56
|
+
const exhaustive = error;
|
|
57
|
+
return exhaustive;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=learning-store.js.map
|
|
@@ -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/manifest.js
CHANGED
|
@@ -54,7 +54,7 @@ function parseManifestFlags(features, knownFlags) {
|
|
|
54
54
|
/**
|
|
55
55
|
* Read and parse the manifest file. Returns null if missing or corrupt.
|
|
56
56
|
*
|
|
57
|
-
* Self-heals the following on-disk inconsistencies
|
|
57
|
+
* Self-heals the following on-disk inconsistencies:
|
|
58
58
|
* - features.kb → features.knowledge rename (both absent → true, D-FEATURES-ABSENT-ON)
|
|
59
59
|
* - features.decisions → features.learning rename (both absent → true, D-FEATURES-ABSENT-ON)
|
|
60
60
|
* - features.flags as string[] → FlagsRecord (via migrateLegacyFlagsToRecord)
|
|
@@ -103,7 +103,7 @@ export async function readManifest(devflowDir) {
|
|
|
103
103
|
const knowledge = typeof features.knowledge === 'boolean' ? features.knowledge
|
|
104
104
|
: typeof features.kb === 'boolean' ? features.kb
|
|
105
105
|
: true;
|
|
106
|
-
// Self-heal: rename features.decisions → features.learning on disk
|
|
106
|
+
// Self-heal: rename features.decisions → features.learning on disk.
|
|
107
107
|
// Coalesce: features.learning wins; fall back to features.decisions; default ON.
|
|
108
108
|
// D-LEARNING-LEGACY-DECISIONS: isMachineFeatureOn and queue_read_gates apply
|
|
109
109
|
// this exact precedence, so the legacy key is honoured before the heal lands.
|
|
@@ -122,7 +122,7 @@ export async function readManifest(devflowDir) {
|
|
|
122
122
|
// `flagsWereLegacy` is true only when the on-disk shape was a string[] (Case A),
|
|
123
123
|
// keeping the needsHeal predicate in lockstep with the parse branch above.
|
|
124
124
|
const { flags: parsedFlags, legacy: flagsWereLegacy } = parseManifestFlags(features, knownFlags);
|
|
125
|
-
//
|
|
125
|
+
// D39: sanitize all values; block prototype pollution keys.
|
|
126
126
|
const sanitizedFlags = sanitizeFlagsRecord(parsedFlags);
|
|
127
127
|
// needsHeal when any legacy artifact is present on disk
|
|
128
128
|
const needsHeal = features.kb !== undefined ||
|
|
@@ -154,14 +154,14 @@ export async function readManifest(devflowDir) {
|
|
|
154
154
|
security: typeof features.security === 'string' && SECURITY_MODES.includes(features.security)
|
|
155
155
|
? features.security
|
|
156
156
|
: undefined,
|
|
157
|
-
// Self-heal: absent proxy field defaults to false
|
|
157
|
+
// Self-heal: absent proxy field defaults to false
|
|
158
158
|
proxy: typeof features.proxy === 'boolean' ? features.proxy : false,
|
|
159
159
|
// Self-heal: absent/malformed compliance → {enabled:false, frameworks:[]}
|
|
160
160
|
compliance: normalizeComplianceFeature(features.compliance),
|
|
161
161
|
// Self-heal: absent/malformed/unknown tracker → {provider:'github'} (AC-3.21).
|
|
162
162
|
// Deliberately NOT in the hard-null set above — see the field's doc comment.
|
|
163
163
|
// Healing here is silent and emits no DEGRADED: that is the correct
|
|
164
|
-
//
|
|
164
|
+
// self-heal behaviour, and a different condition from a per-repo config
|
|
165
165
|
// value outside the domain (which does emit `unknown tracker provider`).
|
|
166
166
|
tracker: normalizeTrackerFeature(features.tracker),
|
|
167
167
|
},
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Pure module — zero I/O. Every question a caller asks about a HOST is answered
|
|
5
5
|
* with a Result; callers own every filesystem call and every process exit.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Pure core-layer module, no build-script or adapter concerns.
|
|
8
8
|
* The registries below are agent-neutral, so what is DERIVED from them is derived
|
|
9
9
|
* here rather than inside an install target — a target adapter computing a build
|
|
10
10
|
* fact, with tests importing that adapter to learn it, is the seam inverting.
|
|
11
|
-
*
|
|
11
|
+
* No process.exit(); every fallible path returns Result. The
|
|
12
12
|
* exiting shell is scripts/build-mds.ts, which renders these errors into its
|
|
13
13
|
* pre-existing messages.
|
|
14
14
|
*
|
|
@@ -85,7 +85,7 @@ export function validateOutputName(name) {
|
|
|
85
85
|
* `tracker/jira/`, and `tracker/mcp.md` would read as a third provider.
|
|
86
86
|
* Relaxing the shared rule instead would have admitted `_anything.md`
|
|
87
87
|
* as a command or an agent basename too — a widening across all three build
|
|
88
|
-
* destinations to buy a property only this one needs (
|
|
88
|
+
* destinations to buy a property only this one needs (classify the case,
|
|
89
89
|
* never blanket-widen).
|
|
90
90
|
*
|
|
91
91
|
* Every other guarantee is inherited by delegation, so the dot-segment,
|
|
@@ -127,8 +127,8 @@ export const AGENTS_OUTPUT_DIR = 'dist/agents';
|
|
|
127
127
|
* the installer decides which skill install triggers the reference overlay from
|
|
128
128
|
* it, and the init summary renders `prefixSkillName()` of it. Retyped at each of
|
|
129
129
|
* those three sites, moving the references to another skill would mean finding
|
|
130
|
-
* all three spellings with nothing failing if only two were found —
|
|
131
|
-
*
|
|
130
|
+
* all three spellings with nothing failing if only two were found — a hardcoded
|
|
131
|
+
* spelling that still resolves.
|
|
132
132
|
*
|
|
133
133
|
* Bare, not `devflow:`-prefixed: the build writes to `dist/skills/git/…` while
|
|
134
134
|
* the install target is `skills/devflow:git/`. prefixSkillName is what spans that
|
|
@@ -162,7 +162,7 @@ const ALLOWED_OUTPUT_DIRS = [
|
|
|
162
162
|
*
|
|
163
163
|
* Exported so guards assert the build's refusal text against the table itself
|
|
164
164
|
* rather than against a retyped literal: adding a destination then rewrites both
|
|
165
|
-
* the message and its assertion from one edit (
|
|
165
|
+
* the message and its assertion from one edit (the expectation must
|
|
166
166
|
* come from the thing under test, not a copy of it).
|
|
167
167
|
*/
|
|
168
168
|
export const ALLOWED_OUTPUT_DIR_NAMES = ALLOWED_OUTPUT_DIRS.map(entry => entry.dir);
|
|
@@ -223,7 +223,7 @@ export function resolveOutputDir(root, declared) {
|
|
|
223
223
|
* reverse alone lets a listed op silently emit nothing.
|
|
224
224
|
*
|
|
225
225
|
* The list is long from its first commit on purpose. A one- or two-element list
|
|
226
|
-
* makes every parity assertion over it vacuous (GAP-42
|
|
226
|
+
* makes every parity assertion over it vacuous (GAP-42) and is
|
|
227
227
|
* structurally identical to the single-arm conditional AC-1.2 forbids, so
|
|
228
228
|
* expandVariants refuses a pair list below MIN_VARIANT_PAIRS.
|
|
229
229
|
*/
|
|
@@ -283,8 +283,8 @@ export const TRACKER_GITHUB_OPS = TRACKER_OPS;
|
|
|
283
283
|
* carries one pointer to each.
|
|
284
284
|
*
|
|
285
285
|
* 9 entries, one above {@link MIN_VARIANT_PAIRS}, which is a floor and not a
|
|
286
|
-
* target: a shorter roster makes every parity assertion over it vacuous (GAP-42
|
|
287
|
-
*
|
|
286
|
+
* target: a shorter roster makes every parity assertion over it vacuous (GAP-42)
|
|
287
|
+
* and `expandVariants` refuses the build, so the roster can grow
|
|
288
288
|
* but never drop below 8. `update-pr-evidence` (#363) is the ninth — it edits the
|
|
289
289
|
* PR body and comments on the PR, both GitHub whatever the tracker is.
|
|
290
290
|
*/
|
|
@@ -558,7 +558,7 @@ export const MIN_VARIANT_PAIRS = 8;
|
|
|
558
558
|
* writes.
|
|
559
559
|
*
|
|
560
560
|
* Pure and total: every refusal is a Result, so the build shell keeps its single
|
|
561
|
-
* exit
|
|
561
|
+
* exit. The expansion is deliberately flat rather than nested —
|
|
562
562
|
* one list of destinations is what the plan pass needs to detect two hosts
|
|
563
563
|
* claiming one file, and a nested shape would have to be flattened there anyway.
|
|
564
564
|
*
|
|
@@ -642,14 +642,14 @@ export function expandVariants(modules = resolveVariantModules()) {
|
|
|
642
642
|
* Lives beside the registry it reads rather than in the Claude Code installer that
|
|
643
643
|
* consumes it: nothing about the answer is Claude-Code-specific, and the packaging
|
|
644
644
|
* and containment tests that read it are asking the BUILD what it emits, not
|
|
645
|
-
* asking an install target
|
|
645
|
+
* asking an install target.
|
|
646
646
|
*
|
|
647
647
|
* Asserts where its siblings return a Result. The registry is a compile-time
|
|
648
648
|
* constant, so a refusal is a programming error rather than an install-time
|
|
649
649
|
* degradation: no caller could sensibly continue, and every caller would otherwise
|
|
650
650
|
* carry the same impossible branch. The full refusal is rendered and not just its
|
|
651
651
|
* `kind` — the payload is what names the offending module and op, and a payload
|
|
652
|
-
* nothing reads is a payload nothing maintains
|
|
652
|
+
* nothing reads is a payload nothing maintains. Same rendering the
|
|
653
653
|
* build's own refusal sinks use (scripts/build-mds.ts).
|
|
654
654
|
*/
|
|
655
655
|
export function generatedReferenceManifest() {
|
|
@@ -721,7 +721,7 @@ export const VARIANT_SECTION_MARKER_RE = /^<!-- op: (_?[a-z0-9][a-z0-9._-]{0,63}
|
|
|
721
721
|
*
|
|
722
722
|
* Bidirectional, and both directions are load-bearing:
|
|
723
723
|
* - unknown-section — the body carries a section for an op the registry does
|
|
724
|
-
* not name, so a file would ship that nothing loads
|
|
724
|
+
* not name, so a file would ship that nothing loads;
|
|
725
725
|
* - missing-section — the registry names an op the body does not cover, so the
|
|
726
726
|
* preamble's load instruction resolves to nothing at runtime.
|
|
727
727
|
* A forward-only check passes on either half of that pair.
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* Discovers routable external models from the routing runtime.
|
|
5
5
|
* The only impure piece of the external-model-routing design.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Pure core-layer module — no adapter imports.
|
|
8
|
+
* cwd = os.tmpdir() so discovery never assumes the devflow
|
|
9
9
|
* directory exists (the --set read path never mkdirs, so the dir may not
|
|
10
10
|
* exist; os.tmpdir() also prevents a stale subswitch.config.json in any
|
|
11
11
|
* particular working directory from causing exit 1 in the routing runtime).
|
|
12
|
-
*
|
|
12
|
+
* discoverExternalModels never throws or rejects — every
|
|
13
13
|
* failure path returns { known: false }.
|
|
14
|
-
*
|
|
14
|
+
* Real-binary tests (T1–T4) live in tests/, never in
|
|
15
15
|
* tests/integration/ which is excluded from npm test by vitest.config.ts.
|
|
16
16
|
*
|
|
17
17
|
* Branding constraint: the routing runtime package name ("subswitch") must
|
|
@@ -508,9 +508,9 @@ function buildRealSpawnAndCollect(logPath) {
|
|
|
508
508
|
* - parse failure on live payload → log reason to proxy.log.
|
|
509
509
|
* - All failures degrade to stale-cache then { known: false } — never throw.
|
|
510
510
|
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
511
|
+
* cwd = os.tmpdir() (not devflow dir, which may not exist on
|
|
512
|
+
* the cold --set path; also prevents legacy subswitch.config.json in cwd
|
|
513
|
+
* from causing exit 1 in the routing runtime).
|
|
514
514
|
*
|
|
515
515
|
* AC-C7: resolveProxyBin() is called live every invocation, never proxy.json.binPath.
|
|
516
516
|
* AC-C8: never throws or rejects.
|
|
@@ -524,7 +524,7 @@ export async function discoverExternalModels(cacheDir, logPath, deps) {
|
|
|
524
524
|
return await _discoverInternal(cacheDir, logPath, deps);
|
|
525
525
|
}
|
|
526
526
|
catch {
|
|
527
|
-
// Catch-all: discoverExternalModels MUST NOT throw
|
|
527
|
+
// Catch-all: discoverExternalModels MUST NOT throw.
|
|
528
528
|
return { known: false };
|
|
529
529
|
}
|
|
530
530
|
}
|
|
@@ -1,109 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file observations.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The ledger entry statuses on the TypeScript side. Pure data: no I/O and no
|
|
5
|
+
* imports, so the HUD can load it without growing its import closure.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* D201:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Re-exported through src/cli/commands/learning.ts for external consumers.
|
|
18
|
-
* Consumed by LedgerRow (this file) and LearningObservation.decisions_status (this file).
|
|
19
|
-
*/
|
|
20
|
-
export const DECISIONS_ENTRY_STATUSES = [
|
|
21
|
-
'Accepted', 'Active', 'Deprecated', 'Superseded', 'Retired',
|
|
22
|
-
];
|
|
23
|
-
/** Valid values for the decisions_status optional field — derived from DECISIONS_ENTRY_STATUSES. */
|
|
24
|
-
const VALID_DECISIONS_STATUSES = new Set(DECISIONS_ENTRY_STATUSES);
|
|
25
|
-
/**
|
|
26
|
-
* Type guard for validating raw JSON as a LearningObservation.
|
|
27
|
-
* Accepts all 4 types (v2: decision + pitfall added) and all statuses including deprecated.
|
|
28
|
-
* New optional fields (anchor_id, date, decisions_status, amendments, raw_body) are
|
|
29
|
-
* validated when present but their absence never causes rejection — backward compatible.
|
|
30
|
-
*/
|
|
31
|
-
export function isLearningObservation(obj) {
|
|
32
|
-
if (typeof obj !== 'object' || obj === null)
|
|
33
|
-
return false;
|
|
34
|
-
const o = obj;
|
|
35
|
-
// Required fields
|
|
36
|
-
if (!(typeof o.id === 'string' && o.id.length > 0))
|
|
37
|
-
return false;
|
|
38
|
-
if (!(o.type === 'workflow' || o.type === 'procedural' || o.type === 'decision' || o.type === 'pitfall'))
|
|
39
|
-
return false;
|
|
40
|
-
if (!(typeof o.pattern === 'string' && o.pattern.length > 0))
|
|
41
|
-
return false;
|
|
42
|
-
if (typeof o.confidence !== 'number')
|
|
43
|
-
return false;
|
|
44
|
-
if (typeof o.observations !== 'number')
|
|
45
|
-
return false;
|
|
46
|
-
if (typeof o.first_seen !== 'string')
|
|
47
|
-
return false;
|
|
48
|
-
if (typeof o.last_seen !== 'string')
|
|
49
|
-
return false;
|
|
50
|
-
if (!(o.status === 'observing' || o.status === 'ready' || o.status === 'created' || o.status === 'deprecated'))
|
|
51
|
-
return false;
|
|
52
|
-
if (!Array.isArray(o.evidence))
|
|
53
|
-
return false;
|
|
54
|
-
if (typeof o.details !== 'string')
|
|
55
|
-
return false;
|
|
56
|
-
// Optional ledger fields: validate type when present, reject if wrong type
|
|
57
|
-
if (o.anchor_id !== undefined && typeof o.anchor_id !== 'string')
|
|
58
|
-
return false;
|
|
59
|
-
if (o.date !== undefined && typeof o.date !== 'string')
|
|
60
|
-
return false;
|
|
61
|
-
if (o.decisions_status !== undefined && !VALID_DECISIONS_STATUSES.has(o.decisions_status))
|
|
62
|
-
return false;
|
|
63
|
-
if (o.amendments !== undefined) {
|
|
64
|
-
if (!Array.isArray(o.amendments))
|
|
65
|
-
return false;
|
|
66
|
-
for (const a of o.amendments) {
|
|
67
|
-
if (typeof a !== 'object' || a === null)
|
|
68
|
-
return false;
|
|
69
|
-
const am = a;
|
|
70
|
-
if (typeof am.date !== 'string' || typeof am.note !== 'string')
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (o.raw_body !== undefined && typeof o.raw_body !== 'string')
|
|
75
|
-
return false;
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Parse a JSONL learning log into typed observations.
|
|
80
|
-
* Skips empty and malformed lines.
|
|
81
|
-
*/
|
|
82
|
-
export function parseLearningLog(logContent) {
|
|
83
|
-
const observations = [];
|
|
84
|
-
for (const line of logContent.split('\n')) {
|
|
85
|
-
const trimmed = line.trim();
|
|
86
|
-
if (!trimmed)
|
|
87
|
-
continue;
|
|
88
|
-
try {
|
|
89
|
-
const parsed = JSON.parse(trimmed);
|
|
90
|
-
if (isLearningObservation(parsed)) {
|
|
91
|
-
observations.push(parsed);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// Skip malformed lines
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return observations;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Parse a JSONL log and return valid observations plus the count of invalid entries.
|
|
102
|
-
* Centralises the raw-line-count + parse pattern used by --status, --list, and --purge.
|
|
8
|
+
* D201: a ledger entry's `decisions_status` takes one of DECISIONS_ENTRY_STATUSES.
|
|
9
|
+
* `Accepted` (decisions) and `Active` (pitfalls) are active and render; the
|
|
10
|
+
* INACTIVE_DECISIONS_STATUSES stay in the ledger and leave the rendered files and
|
|
11
|
+
* the HUD counts; an absent or unknown status counts as active. These lists
|
|
12
|
+
* mirror ACTIVE_STATUSES / INACTIVE_STATUSES in
|
|
13
|
+
* src/assets/scripts/hooks/lib/learning-store.cjs, which the hooks and the
|
|
14
|
+
* renderer use, and the parity test in tests/decisions/learning-store.test.ts
|
|
15
|
+
* pins the two equal. Reason: the renderer, the HUD and this module each kept a
|
|
16
|
+
* list of their own, so a status added to one counted as active in the others.
|
|
103
17
|
*/
|
|
104
|
-
export
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
18
|
+
export const INACTIVE_DECISIONS_STATUSES = ['Encoded', 'Superseded', 'Retired', 'Deprecated'];
|
|
19
|
+
export const DECISIONS_ENTRY_STATUSES = ['Accepted', 'Active', ...INACTIVE_DECISIONS_STATUSES];
|
|
20
|
+
const INACTIVE_DECISIONS_STATUS_SET = new Set(INACTIVE_DECISIONS_STATUSES);
|
|
21
|
+
/** True unless `status` is one of INACTIVE_DECISIONS_STATUSES (D201). */
|
|
22
|
+
export function isActiveDecisionsStatus(status) {
|
|
23
|
+
return !status || !INACTIVE_DECISIONS_STATUS_SET.has(status);
|
|
108
24
|
}
|
|
109
25
|
//# sourceMappingURL=observations.js.map
|
|
@@ -61,8 +61,8 @@ export function mdEntryName(entry) {
|
|
|
61
61
|
*
|
|
62
62
|
* Per-item failure isolation: both the outer readdir and the inner rm are
|
|
63
63
|
* independently try/caught — a missing directory is a no-op and a failed
|
|
64
|
-
* individual removal is recorded in `failed` without aborting the sweep
|
|
65
|
-
* Never writes, only removes
|
|
64
|
+
* individual removal is recorded in `failed` without aborting the sweep.
|
|
65
|
+
* Never writes, only removes — no delete-then-write window.
|
|
66
66
|
*/
|
|
67
67
|
export async function sweepOrphanedAssets(dir, knownNames, extractRegistryName) {
|
|
68
68
|
const removed = [];
|
|
@@ -81,12 +81,12 @@ export async function sweepOrphanedAssets(dir, knownNames, extractRegistryName)
|
|
|
81
81
|
removed.push(registryName);
|
|
82
82
|
}
|
|
83
83
|
catch (err) {
|
|
84
|
-
failed.push({ name: registryName, error: err }); /* per-item isolation
|
|
84
|
+
failed.push({ name: registryName, error: err }); /* per-item isolation */
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
catch { /* directory absent or unreadable — not an error
|
|
89
|
+
catch { /* directory absent or unreadable — not an error */ }
|
|
90
90
|
return { scanned, removed, failed };
|
|
91
91
|
}
|
|
92
92
|
//# sourceMappingURL=orphan-sweep.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
|
{
|
|
@@ -465,7 +471,7 @@ export const DELETED_PLUGIN_NAMES = [
|
|
|
465
471
|
* - uninstall.ts sweepDevflowNamespaces: union into knownNames to spare devflow:compliance
|
|
466
472
|
* from the post-selective-uninstall sweep (nothing converges after selective uninstall)
|
|
467
473
|
* - skills.ts: union into allSkills for shadow/unshadow/list
|
|
468
|
-
* - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap
|
|
474
|
+
* - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap)
|
|
469
475
|
*
|
|
470
476
|
* D-FO-1: FEATURE_OWNED_SKILLS must be disjoint from getAllSkillNames()
|
|
471
477
|
* (guarded by plugins.test.ts FEATURE_OWNED constants describe block).
|
|
@@ -478,7 +484,7 @@ export const FEATURE_OWNED_SKILLS = ['compliance'];
|
|
|
478
484
|
*
|
|
479
485
|
* Used by:
|
|
480
486
|
* - rules.ts: union into allRules for shadow/unshadow/list
|
|
481
|
-
* - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap
|
|
487
|
+
* - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap)
|
|
482
488
|
*
|
|
483
489
|
* D-FO-2: FEATURE_OWNED_RULES must be disjoint from getAllRuleNames()
|
|
484
490
|
* (guarded by plugins.test.ts FEATURE_OWNED constants describe block).
|
|
@@ -511,7 +517,7 @@ export const PRESENCE_GATED_SKILLS = [
|
|
|
511
517
|
* The classified exception to the closure guard — declared HERE, at the
|
|
512
518
|
* declaration site of the field it exempts, and imported by the guard.
|
|
513
519
|
*
|
|
514
|
-
* D-TEMPLATE-EXCEPTION (
|
|
520
|
+
* D-TEMPLATE-EXCEPTION (one authority for a prohibition and its
|
|
515
521
|
* exemptions). Every other templated reference in the corpus carries a literal
|
|
516
522
|
* prefix and resolves through it — `devflow:research-{RESEARCH_TYPE}` resolves
|
|
517
523
|
* because five in-scope skills start with `research-`. The Review focus skill is
|
|
@@ -745,8 +751,7 @@ export function buildFullSkillsMap() {
|
|
|
745
751
|
* add X, not a statement that X is the whole selection, so it may never remove
|
|
746
752
|
* what another plugin contributed (AC-22). {@link FEATURE_OWNED_SKILLS} is
|
|
747
753
|
* subtracted unconditionally: those install and uninstall with their feature,
|
|
748
|
-
* and sweeping them here would delete an artifact this code does not own
|
|
749
|
-
* (applies ADR-024).
|
|
754
|
+
* and sweeping them here would delete an artifact this code does not own.
|
|
750
755
|
*
|
|
751
756
|
* A shadow is NEVER removed, whatever the selection — `~/.devflow/skills/` is
|
|
752
757
|
* user content. One that falls outside the install set simply applies to
|
|
@@ -46,10 +46,14 @@ export function getFeatureConfigPath(projectRoot) {
|
|
|
46
46
|
export function getLearningPendingTurnsPath(projectRoot) {
|
|
47
47
|
return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.jsonl');
|
|
48
48
|
}
|
|
49
|
-
/** .devflow/learning/.pending-turns.processing —
|
|
49
|
+
/** .devflow/learning/.pending-turns.processing — the claimed batch a Learning run holds while it processes it */
|
|
50
50
|
export function getLearningPendingTurnsProcessingPath(projectRoot) {
|
|
51
51
|
return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.processing');
|
|
52
52
|
}
|
|
53
|
+
/** .devflow/learning/.pending-turns.owner — the token of the run that holds the claim (learning-store.cjs) */
|
|
54
|
+
export function getLearningClaimOwnerPath(projectRoot) {
|
|
55
|
+
return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.owner');
|
|
56
|
+
}
|
|
53
57
|
// ---------------------------------------------------------------------------
|
|
54
58
|
// Learning content files
|
|
55
59
|
// ---------------------------------------------------------------------------
|
|
@@ -77,26 +81,18 @@ export function getDecisionsLogPath(projectRoot) {
|
|
|
77
81
|
export function getDecisionsArchivePath(projectRoot) {
|
|
78
82
|
return path.join(projectRoot, '.devflow', 'learning', 'decisions-log.archive.jsonl');
|
|
79
83
|
}
|
|
84
|
+
/** .devflow/learning/decisions-history.jsonl — prior content versions of rewritten entries (learning-store.cjs) */
|
|
85
|
+
export function getDecisionsHistoryPath(projectRoot) {
|
|
86
|
+
return path.join(projectRoot, '.devflow', 'learning', 'decisions-history.jsonl');
|
|
87
|
+
}
|
|
80
88
|
/** .devflow/learning/.decisions.lock — mkdir-based lock directory */
|
|
81
89
|
export function getDecisionsLockDir(projectRoot) {
|
|
82
90
|
return path.join(projectRoot, '.devflow', 'learning', '.decisions.lock');
|
|
83
91
|
}
|
|
84
|
-
/** .devflow/learning/.decisions-usage.json */
|
|
85
|
-
export function getDecisionsUsagePath(projectRoot) {
|
|
86
|
-
return path.join(projectRoot, '.devflow', 'learning', '.decisions-usage.json');
|
|
87
|
-
}
|
|
88
|
-
/** .devflow/learning/.decisions-usage.lock/ — mkdir-based lock directory for usage file */
|
|
89
|
-
export function getDecisionsUsageLockDir(projectRoot) {
|
|
90
|
-
return path.join(projectRoot, '.devflow', 'learning', '.decisions-usage.lock');
|
|
91
|
-
}
|
|
92
92
|
/** .devflow/learning/index.md — pre-rendered compact index written by render-decisions.cjs */
|
|
93
93
|
export function getDecisionsIndexPath(projectRoot) {
|
|
94
94
|
return path.join(projectRoot, '.devflow', 'learning', 'index.md');
|
|
95
95
|
}
|
|
96
|
-
/** .devflow/learning/.observations.lock — mkdir-based lock directory for observation log writes */
|
|
97
|
-
export function getObservationsLockDir(projectRoot) {
|
|
98
|
-
return path.join(projectRoot, '.devflow', 'learning', '.observations.lock');
|
|
99
|
-
}
|
|
100
96
|
// ---------------------------------------------------------------------------
|
|
101
97
|
// Memory / working-memory files
|
|
102
98
|
// ---------------------------------------------------------------------------
|
package/dist/core/proxy-log.js
CHANGED
|
@@ -10,8 +10,8 @@ import * as path from 'path';
|
|
|
10
10
|
* - openProxyLog — 0700-parent + 0600-file open with best-effort chmod (SEC-2)
|
|
11
11
|
* - rotateProxyLogIfLarge — pre-spawn-only 2MB→1MB rotation that preserves 0600 mode
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Every failure path is non-fatal (wrapped in try/catch); one bad
|
|
14
|
+
* chmod or rotation error must never abort the enable flow.
|
|
15
15
|
*/
|
|
16
16
|
/** 2 MB max log size before rotation (mirrors ensure-proxy _LOG_MAX_BYTES). */
|
|
17
17
|
export const PROXY_LOG_MAX_BYTES = 2_097_152;
|
|
@@ -28,7 +28,7 @@ export const PROXY_LOG_TAIL_BYTES = 1_048_576;
|
|
|
28
28
|
* Call sites compose on top of this result to add process-specific vars
|
|
29
29
|
* (e.g. SUBSWITCH_CONFIG for the relay spawn and doctor spawn).
|
|
30
30
|
*
|
|
31
|
-
*
|
|
31
|
+
* On the paths devflow
|
|
32
32
|
* invokes (serve, doctor, models) the routing runtime reads ANTHROPIC_API_KEY,
|
|
33
33
|
* FORCE_COLOR, NO_COLOR (dist/tty.js, presence semantics), SUBSWITCH_CONFIG and
|
|
34
34
|
* XDG_CONFIG_HOME (user-config lookup, reached only on an implicit config load —
|
|
@@ -42,8 +42,8 @@ export const PROXY_LOG_TAIL_BYTES = 1_048_576;
|
|
|
42
42
|
* NODE_EXTRA_CA_CERTS is included so corporate-TLS deployments can supply a CA
|
|
43
43
|
* bundle; it is omitted when unset (the absent-→-omit loop handles this).
|
|
44
44
|
* NODE_OPTIONS is deliberately excluded: it permits arbitrary code execution via
|
|
45
|
-
* --require/--import. Mirrored in the ensure-proxy bash hook's _RELAY_ENV array
|
|
46
|
-
*
|
|
45
|
+
* --require/--import. Mirrored in the ensure-proxy bash hook's _RELAY_ENV array;
|
|
46
|
+
* both allowlists must be kept in sync.
|
|
47
47
|
*/
|
|
48
48
|
export function scrubChildEnv() {
|
|
49
49
|
const POSIX_ALLOWLIST = ['PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL', 'NODE_EXTRA_CA_CERTS'];
|
|
@@ -71,7 +71,7 @@ export function scrubChildEnv() {
|
|
|
71
71
|
* 3. best-effort fs.chmod(logPath, 0o600) — widens a pre-existing file
|
|
72
72
|
* that was created before this guard existed (e.g. mode 0644 on disk).
|
|
73
73
|
* Non-fatal: a chmod failure (EPERM, ENOENT race) must never prevent the
|
|
74
|
-
* handle from being returned
|
|
74
|
+
* handle from being returned.
|
|
75
75
|
* Follows the SEC-1 precedent in src/core/fs-atomic.ts (commit 5755d56):
|
|
76
76
|
* chmod in try/catch, never fatal, with a comment citing the rationale.
|
|
77
77
|
*
|
|
@@ -88,7 +88,7 @@ export async function openProxyLog(logPath) {
|
|
|
88
88
|
const handle = await fs.open(logPath, 'a', 0o600);
|
|
89
89
|
// Step 3: best-effort chmod for a pre-existing file that has a wider mode.
|
|
90
90
|
// Non-fatal: chmod failure must not prevent the handle from being returned.
|
|
91
|
-
//
|
|
91
|
+
// Failure isolation — one bad chmod must never abort the enable flow.
|
|
92
92
|
try {
|
|
93
93
|
await fs.chmod(logPath, 0o600);
|
|
94
94
|
}
|
|
@@ -120,7 +120,7 @@ export async function openProxyLog(logPath) {
|
|
|
120
120
|
*
|
|
121
121
|
* Non-fatal: rotation failure (disk full, EPERM, read error) leaves the
|
|
122
122
|
* original log untouched and the enable proceeds with an oversized log.
|
|
123
|
-
*
|
|
123
|
+
* One rotation error must never abort the enable flow.
|
|
124
124
|
*
|
|
125
125
|
* @param logPath - Absolute path to proxy.log.
|
|
126
126
|
*/
|
package/dist/core/proxy-state.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Proxy state persistence and routing config helpers for the Devflow external
|
|
3
3
|
* model routing feature.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Pure core-layer module — no Claude Code adapter concerns.
|
|
6
|
+
* Never call process.exit() inside finally-guarded scopes; all
|
|
7
7
|
* fallible operations return Result instead of throwing.
|
|
8
8
|
*
|
|
9
9
|
* State file: ~/.devflow/proxy.json
|
|
@@ -155,7 +155,7 @@ const ROUTING_CONFIG_REJECTED_SUBKEYS = {
|
|
|
155
155
|
*
|
|
156
156
|
* If `existingContent` is missing or malformed, falls back to a port-only config.
|
|
157
157
|
*
|
|
158
|
-
*
|
|
158
|
+
* Pure core-layer function — no I/O.
|
|
159
159
|
* @D-EFR-4: see note above for the strict top-key constraint.
|
|
160
160
|
*/
|
|
161
161
|
export function buildRoutingConfigJson(port, existingContent) {
|