devflow-kit 2.2.0 → 2.4.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 +45 -0
- package/dist/cli/commands/attribution-prompts.js +144 -0
- package/dist/cli/commands/compliance-prompts.js +7 -11
- package/dist/cli/commands/init-seed.js +45 -4
- package/dist/cli/commands/init.js +49 -6
- package/dist/cli/commands/prompt-io.js +38 -0
- package/dist/cli/commands/proxy.js +91 -31
- package/dist/cli/commands/uninstall.js +22 -12
- package/dist/commands/resolve.md +29 -16
- package/dist/core/flags.js +145 -4
- package/dist/core/proxy-log.js +4 -2
- package/dist/core/proxy-state.js +91 -37
- package/dist/targets/claude-code/post-install.js +106 -29
- package/package.json +2 -2
- package/src/assets/agents/learning.md +63 -13
- package/src/assets/agents/triage.md +19 -1
- package/src/assets/commands/resolve.mds +29 -16
- package/src/assets/scripts/hooks/background-memory-update +180 -37
- package/src/assets/scripts/hooks/ensure-proxy +64 -5
- package/src/assets/scripts/hooks/is-hex-sha +14 -0
- package/src/assets/scripts/hooks/json-helper.cjs +223 -38
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +264 -43
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +1 -1
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +59 -28
- package/src/assets/scripts/hooks/pre-compact-memory +40 -22
- package/src/assets/scripts/hooks/session-start-memory +19 -20
- package/src/assets/skills/git/SKILL.md +1 -5
- package/src/assets/skills/git/references/patterns.md +0 -6
- package/src/targets/claude-code/templates/settings.json +0 -4
|
@@ -15,8 +15,8 @@ import { removeCaptureHooks } from './capture.js';
|
|
|
15
15
|
import { removeDreamHook } from './legacy-hooks.js';
|
|
16
16
|
import { removeHudStatusLine } from './hud.js';
|
|
17
17
|
import { removeContextHook } from './context.js';
|
|
18
|
-
import {
|
|
19
|
-
import { readProxyState,
|
|
18
|
+
import { applyProxyTeardownToSettings } from './proxy.js';
|
|
19
|
+
import { readProxyState, proxyJsonExists } from '../../core/proxy-state.js';
|
|
20
20
|
import { hudCacheDir } from '../../core/cache.js';
|
|
21
21
|
import { revertExternalAgents } from '../../core/agent-models.js';
|
|
22
22
|
import { detectShell, getProfilePath } from '../../core/safe-delete.js';
|
|
@@ -768,14 +768,16 @@ export async function runCleanupPhase(opts) {
|
|
|
768
768
|
settingsContent = stripFlags(settingsContent); // also strips viewMode via view-mode registry entry
|
|
769
769
|
settingsContent = stripDevflowTeammateModeFromJson(settingsContent);
|
|
770
770
|
// Remove proxy hooks and ANTHROPIC_BASE_URL env in a single parse-mutate-serialize pass.
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
771
|
+
// D-STRIP-1: an absent entry in managedProxyPorts means proxy.json did not exist
|
|
772
|
+
// before artifact removal — no evidence Devflow ever wrote the env vars, so
|
|
773
|
+
// applyProxyTeardownToSettings removes only our hooks and leaves the user's
|
|
774
|
+
// ANTHROPIC_BASE_URL / CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT alone.
|
|
775
|
+
// Hook removal is never gated: our hooks must not survive an uninstall.
|
|
776
|
+
// REG-1: the port-scoped URL strip protects any foreign localhost gateway on a
|
|
777
|
+
// different port — the managedPort value is from the pre-captured proxy.json port.
|
|
775
778
|
{
|
|
776
|
-
const managedPort = opts.managedProxyPorts?.get(scope) ?? DEFAULT_PROXY_PORT;
|
|
777
779
|
const parsedSettings = JSON.parse(settingsContent);
|
|
778
|
-
|
|
780
|
+
applyProxyTeardownToSettings(parsedSettings, opts.managedProxyPorts?.get(scope));
|
|
779
781
|
settingsContent = JSON.stringify(parsedSettings, null, 2) + '\n';
|
|
780
782
|
}
|
|
781
783
|
if (settingsContent !== originalContent) {
|
|
@@ -977,16 +979,24 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
977
979
|
// Pre-capture managed proxy ports for each scope BEFORE artifact removal.
|
|
978
980
|
// proxy.json is deleted by removeDevFlowInstallArtifacts (inside runFullPhaseForScope),
|
|
979
981
|
// so reading it after the full phase would always yield DEFAULT_PROXY_PORT. (F6)
|
|
982
|
+
//
|
|
983
|
+
// D-STRIP-1: only populate managedProxyPorts when proxy.json exists — evidence that
|
|
984
|
+
// Devflow has managed the proxy on this machine. runCleanupPhase gates the
|
|
985
|
+
// applyProxyTeardownToSettings call on the port being present in the map, so when the file
|
|
986
|
+
// is absent (never managed), no env strip is attempted and a user's own gateway vars
|
|
987
|
+
// are left untouched.
|
|
980
988
|
const managedProxyPorts = new Map();
|
|
981
989
|
if (!isSelectiveUninstall) {
|
|
982
990
|
for (const scope of scopesToUninstall) {
|
|
983
991
|
try {
|
|
984
992
|
const paths = await getInstallationPaths(scope);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
993
|
+
if (await proxyJsonExists(paths.devflowDir)) {
|
|
994
|
+
const proxyState = await readProxyState(paths.devflowDir);
|
|
995
|
+
if (proxyState.ok)
|
|
996
|
+
managedProxyPorts.set(scope, proxyState.value.port);
|
|
997
|
+
}
|
|
988
998
|
}
|
|
989
|
-
catch { /* non-fatal
|
|
999
|
+
catch { /* non-fatal */ }
|
|
990
1000
|
}
|
|
991
1001
|
}
|
|
992
1002
|
// Uninstall from each scope
|
package/dist/commands/resolve.md
CHANGED
|
@@ -50,7 +50,7 @@ In multi-worktree mode, spawn all pre-flight agents **in a single message** (par
|
|
|
50
50
|
|
|
51
51
|
**If BLOCKED:** In single-worktree mode, stop and report the blocker to user. If no reviews found, suggest `/code-review` or `/bug-analysis` first. In multi-worktree mode, report the failure but continue with other worktrees.
|
|
52
52
|
|
|
53
|
-
**Extract from response:** `branch`, `branch_slug`, `pr_number`, `review_count`, `diff_files` per worktree.
|
|
53
|
+
**Extract from response:** `branch`, `base_branch`, `branch_slug`, `pr_number`, `review_count`, `diff_files` per worktree.
|
|
54
54
|
|
|
55
55
|
**Fetch PR body** (after extracting `pr_number`):
|
|
56
56
|
```bash
|
|
@@ -186,7 +186,7 @@ Issues are extracted from `{TARGET_DIR}` only — never cross-reference reviews
|
|
|
186
186
|
### Phase 1b: Fetch External Review Threads (Compliance-gated)
|
|
187
187
|
|
|
188
188
|
**Produces:** THREAD_MAP
|
|
189
|
-
**Requires:**
|
|
189
|
+
**Requires:** BRANCH_INFO, COMPLIANCE_SKILL_INSTALLED
|
|
190
190
|
|
|
191
191
|
Skip this phase if `COMPLIANCE_SKILL_INSTALLED` is false.
|
|
192
192
|
|
|
@@ -205,7 +205,7 @@ Parse `THREAD_MAP` from Git agent output. If Git agent returns `TRACEABILITY: DE
|
|
|
205
205
|
### Phase 2: Global Triage
|
|
206
206
|
|
|
207
207
|
**Produces:** TRIAGE_RESULTS
|
|
208
|
-
**Requires:** ISSUES, DIFF_FILES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, PR_DESCRIPTION
|
|
208
|
+
**Requires:** ISSUES, DIFF_FILES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, PR_DESCRIPTION
|
|
209
209
|
|
|
210
210
|
Spawn a single global Triage agent for ALL issues:
|
|
211
211
|
|
|
@@ -217,7 +217,7 @@ WORKTREE_PATH: {worktree_path} (omit if cwd)
|
|
|
217
217
|
DECISIONS_CONTEXT: {decisions_context}
|
|
218
218
|
FEATURE_KNOWLEDGE: {feature_knowledge}
|
|
219
219
|
PR_DESCRIPTION: <pr-description>{pr_description}</pr-description>
|
|
220
|
-
Triage every issue
|
|
220
|
+
Triage every issue: collapse duplicates first, then apply the blast-radius disposition matrix to each group's primary. Assign exactly one verdict per issue.
|
|
221
221
|
Follow devflow:apply-decisions to Read full ADR/PF bodies on demand.
|
|
222
222
|
Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE."
|
|
223
223
|
```
|
|
@@ -229,11 +229,12 @@ Wait for Triage agent to complete before proceeding. Parse verdict ledger from T
|
|
|
229
229
|
- **BY_DESIGN**: Intentional code (with ADR or code doc citation)
|
|
230
230
|
- **FIX_SEPARATE**: Valid but out of blast-radius scope (must become manage-debt ticket)
|
|
231
231
|
- **TECH_DEBT**: Architectural overhaul only — LAST RESORT
|
|
232
|
+
- **DUPLICATE**: Collapsed duplicate issue — carries `duplicate_of: <primary-id>` referencing the non-DUPLICATE primary; inherits the primary's outcome
|
|
232
233
|
|
|
233
234
|
Collect all decisions citations (ADR-NNN / PF-NNN) from Triage agent Reasoning columns.
|
|
234
235
|
|
|
235
236
|
**Triage agent completeness assertion (avoids PF-002):** Verify the parsed ledger against ISSUES before proceeding:
|
|
236
|
-
1. Every issue `id` from ISSUES must appear in exactly one verdict bucket — none may vanish, none may appear in multiple buckets.
|
|
237
|
+
1. Every issue `id` from ISSUES must appear in exactly one verdict bucket — none may vanish, none may appear in multiple buckets. DUPLICATE is a valid bucket; a valid DUPLICATE entry must name its `duplicate_of` primary (the `Duplicate Of` column of the ledger's DUPLICATE table) and that primary must be a non-DUPLICATE issue id. A missing `duplicate_of` or one that chains to another DUPLICATE is a **Triage agent failure** (retry-then-abort as below).
|
|
237
238
|
2. If the Triage agent output is empty, contains a skill re-entrancy guard string (e.g., contains `already running`), or is missing any issue IDs from ISSUES: treat as a **Triage agent failure**:
|
|
238
239
|
- Retry the Triage agent once with the same inputs.
|
|
239
240
|
- If the retry also fails the completeness check: abort with a clear error message listing the missing issue IDs and failure reason — never proceed with dropped issues.
|
|
@@ -247,7 +248,7 @@ Collect all decisions citations (ADR-NNN / PF-NNN) from Triage agent Reasoning c
|
|
|
247
248
|
|
|
248
249
|
If FIX_NOW list is empty: skip to Phase 5 — write full summary (Phase 5), run manage-debt (Phase 9) if FIX_SEPARATE/TECH_DEBT exist, run thread resolution + resolution comment (Phase 9b), run merge readiness (Phase 9c), display results (Phase 10).
|
|
249
250
|
|
|
250
|
-
Otherwise, batch FIX_NOW issues for Code agent execution:
|
|
251
|
+
Otherwise, batch FIX_NOW issues for Code agent execution. **DUPLICATE issues are never dispatched** — they inherit the primary's outcome:
|
|
251
252
|
- **Same-file issues** → one batch (one Code agent per file, sequential for same-file pairs)
|
|
252
253
|
- **Distinct-file issues** → parallel Code agents
|
|
253
254
|
- **Max 5 issues per batch** — chunk large sets
|
|
@@ -255,7 +256,7 @@ Otherwise, batch FIX_NOW issues for Code agent execution:
|
|
|
255
256
|
### Phase 4: Fix (Code agent × N)
|
|
256
257
|
|
|
257
258
|
**Produces:** CODE_AGENT_RESULTS
|
|
258
|
-
**Requires:** BATCHES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE
|
|
259
|
+
**Requires:** BATCHES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE
|
|
259
260
|
|
|
260
261
|
For each batch, spawn Code agent with `OPERATION: issue-fix` and `PUSH: false`:
|
|
261
262
|
|
|
@@ -284,12 +285,14 @@ Collect from each Code agent:
|
|
|
284
285
|
### Phase 5: Write resolution-summary.md
|
|
285
286
|
|
|
286
287
|
**Produces:** RESOLUTION_FILE (early write for compaction safety)
|
|
287
|
-
**Requires:** TRIAGE_RESULTS, CODE_AGENT_RESULTS
|
|
288
|
+
**Requires:** TRIAGE_RESULTS, CODE_AGENT_RESULTS, TARGET_DIR, BRANCH_INFO
|
|
288
289
|
|
|
289
290
|
**Immediately write `resolution-summary.md`** to `{TARGET_DIR}` using the Write tool. Do this now — not in Phase 9 — while results are fresh in context. This ensures the record is persisted even if later phases (Simplify, Verification Gate, CI gate, Tech Debt) trigger context compaction.
|
|
290
291
|
|
|
291
292
|
Set `Tracked` for FIX_SEPARATE and TECH_DEBT items to `(pending)` — to be backfilled after Phase 9 manage-debt.
|
|
292
293
|
|
|
294
|
+
DUPLICATE issues are listed **only** in `## Duplicates` — never in `## Fixed Issues`, `## False Positives`, `## By Design`, `## Fix Separately`, `## Deferred to Tech Debt`, `## Escalations`, or `## Blocked`. A duplicate of a FALSE_POSITIVE primary therefore leaves only the primary in the `False Positive` row and the `## False Positives` section; the same holds for every other outcome the duplicate inherits.
|
|
295
|
+
|
|
293
296
|
Use the template from the Output Artifact section below.
|
|
294
297
|
|
|
295
298
|
### Phase 6: Simplify
|
|
@@ -382,7 +385,7 @@ Otherwise, for each worktree with fixes:
|
|
|
382
385
|
|
|
383
386
|
**IMPORTANT**: Run sequentially across all worktrees (not in parallel) to avoid GitHub API conflicts.
|
|
384
387
|
|
|
385
|
-
If any issues are FIX_SEPARATE or TECH_DEBT, spawn Git agent:
|
|
388
|
+
If any issues are FIX_SEPARATE or TECH_DEBT, spawn Git agent. **DUPLICATE issues never create their own debt tickets** — a duplicate of a deferred primary is covered by the primary's ticket:
|
|
386
389
|
|
|
387
390
|
```
|
|
388
391
|
Agent(subagent_type="Git"):
|
|
@@ -407,6 +410,7 @@ Skip this step if `COMPLIANCE_SKILL_INSTALLED` is false or THREAD_MAP is empty.
|
|
|
407
410
|
|
|
408
411
|
Prepare THREAD_MAP with verdicts from triage/code agent results:
|
|
409
412
|
- For each `ext-{N}`: match to an issue verdict (FIXED, FALSE_POSITIVE, BY_DESIGN, ESCALATED) by `file:line` correlation
|
|
413
|
+
- If the matched issue has verdict DUPLICATE, use the **primary's** verdict and verification status for the reply — do not expose DUPLICATE to the thread author (avoids PF-024; caller-side mapping — git.md contracts unchanged)
|
|
410
414
|
- Include `commit_sha` from Code agent results for FIXED verdicts
|
|
411
415
|
- Unmatched threads: ESCALATED (human review)
|
|
412
416
|
|
|
@@ -447,7 +451,7 @@ Update `## Third-Party Threads` section in resolution-summary.md with thread res
|
|
|
447
451
|
### Phase 9c: Merge Readiness (Compliance-gated, Report-only)
|
|
448
452
|
|
|
449
453
|
**Produces:** MERGE_READINESS_REPORT
|
|
450
|
-
**Requires:**
|
|
454
|
+
**Requires:** BRANCH_INFO, COMPLIANCE_SKILL_INSTALLED
|
|
451
455
|
|
|
452
456
|
Skip this phase if `COMPLIANCE_SKILL_INSTALLED` is false.
|
|
453
457
|
|
|
@@ -466,7 +470,7 @@ If Git agent returns `TRACEABILITY: DEGRADED`: warn, proceed to Phase 10.
|
|
|
466
470
|
|
|
467
471
|
### Phase 10: Report
|
|
468
472
|
|
|
469
|
-
**Requires:** TARGET_DIR
|
|
473
|
+
**Requires:** BRANCH_INFO, TARGET_DIR
|
|
470
474
|
|
|
471
475
|
The resolution summary was already written to `{TARGET_DIR}/resolution-summary.md` in Phase 5 (updated by Phase 7 and Phase 9). Display results to the user:
|
|
472
476
|
|
|
@@ -486,6 +490,7 @@ The resolution summary was already written to `{TARGET_DIR}/resolution-summary.m
|
|
|
486
490
|
| Deferred | {n} |
|
|
487
491
|
| Blocked | {n} |
|
|
488
492
|
| Escalated | {n} |
|
|
493
|
+
| Duplicates Collapsed | {n} |
|
|
489
494
|
|
|
490
495
|
### Verification
|
|
491
496
|
Final gate: {PASS | FAILED after N attempts}
|
|
@@ -578,9 +583,9 @@ After writing, commit the two files to the current worktree branch yourself by r
|
|
|
578
583
|
│ └─ Git agent (fetch-review-threads) → THREAD_MAP
|
|
579
584
|
│
|
|
580
585
|
├─ Phase 2: Global Triage [Triage agent, opus, single agent]
|
|
581
|
-
│ └─ ALL issues → verdict ledger by disposition
|
|
586
|
+
│ └─ ALL issues → verdict ledger by disposition (incl. DUPLICATE with duplicate_of)
|
|
582
587
|
│
|
|
583
|
-
├─ Phase 3: Batch FIX_NOW issues (skip if empty)
|
|
588
|
+
├─ Phase 3: Batch FIX_NOW issues (skip if empty; DUPLICATE issues never dispatched)
|
|
584
589
|
│ └─ same-file sequential, distinct-file parallel, max 5/batch
|
|
585
590
|
│
|
|
586
591
|
├─ Phase 4: Fix [Code agent × N, OPERATION: issue-fix, PUSH: false]
|
|
@@ -623,6 +628,8 @@ After writing, commit the two files to the current worktree branch yourself by r
|
|
|
623
628
|
| Worktree pre-flight fails | Report failure, continue with other worktrees |
|
|
624
629
|
| Empty FIX_NOW list | Skip Phases 3-4/6-8; still write full summary + run manage-debt if FIX_SEPARATE/TECH_DEBT exist |
|
|
625
630
|
| ESCALATED security issues | Surfaced in ## Escalations + display callout; never routed to manage-debt |
|
|
631
|
+
| DUPLICATE verdict without duplicate_of, or chained to another DUPLICATE | Treated as Triage failure — same retry-then-abort as a vanished id |
|
|
632
|
+
| DUPLICATE issues in THREAD_MAP | Map ext-{N} to primary's verdict/verification status for thread reply |
|
|
626
633
|
| Verification Gate FAILED after 2 attempts | Recorded as FAILED in ## Verification + blocking callout; CI gate skipped; proceed to Phase 9 (manage-debt) then Phase 10 (display) |
|
|
627
634
|
| gh/GitHub absent | manage-debt fails gracefully; Tracked stays "(pending)" + noted — recorded, not dropped |
|
|
628
635
|
| COMPLIANCE_SKILL_INSTALLED false | Phases 1b, 9b-step-1, and 9c are skipped; post-resolution-summary (Phase 9b step 2) still runs if a PR is known |
|
|
@@ -651,7 +658,7 @@ Written in Phase 5 (Collect Results) to `{TARGET_DIR}/resolution-summary.md`:
|
|
|
651
658
|
```markdown
|
|
652
659
|
# Resolution Summary
|
|
653
660
|
|
|
654
|
-
**Branch**: {branch} -> {
|
|
661
|
+
**Branch**: {branch} -> {base_branch}
|
|
655
662
|
**Date**: {timestamp}
|
|
656
663
|
**Review**: {TARGET_DIR}
|
|
657
664
|
**Command**: /resolve
|
|
@@ -673,8 +680,9 @@ Written in Phase 5 (Collect Results) to `{TARGET_DIR}/resolution-summary.md`:
|
|
|
673
680
|
| Deferred | {n} |
|
|
674
681
|
| Blocked | {n} |
|
|
675
682
|
| Escalated | {n} |
|
|
683
|
+
| Duplicates Collapsed | {n} |
|
|
676
684
|
|
|
677
|
-
_(Note: `Deferred` = `## Fix Separately` count + `## Deferred to Tech Debt` count combined — the two sections are distinct by scope, but the Statistics row aggregates both for the convergence parser.)_
|
|
685
|
+
_(Note: `Deferred` = `## Fix Separately` count + `## Deferred to Tech Debt` count combined — the two sections are distinct by scope, but the Statistics row aggregates both for the convergence parser. `Total Issues` counts every triaged issue including collapsed duplicates; every row **between** `Total Issues` and `Duplicates Collapsed` counts UNIQUE (non-DUPLICATE) issues only, so `Total Issues` equals the sum of the rows below it. Excluding duplicates from `Fixed`, `False Positive`, and `Deferred` de-skews the fp\_ratio convergence formula in code-review without any parser change.)_
|
|
678
686
|
|
|
679
687
|
## Verification
|
|
680
688
|
| Command | Result |
|
|
@@ -720,6 +728,11 @@ Final gate: PASS | FAILED after {n} attempts
|
|
|
720
728
|
|-------|-----------|---------|
|
|
721
729
|
| {description} | {file}:{line} | {why} |
|
|
722
730
|
|
|
731
|
+
## Duplicates
|
|
732
|
+
| Issue | Duplicate Of | File:Line |
|
|
733
|
+
|-------|-------------|-----------|
|
|
734
|
+
| {description} | {primary-id} | {file}:{line} |
|
|
735
|
+
|
|
723
736
|
## Third-Party Threads
|
|
724
737
|
| Thread | File:Line | Verdict | Status |
|
|
725
738
|
|--------|-----------|---------|--------|
|
|
@@ -728,4 +741,4 @@ Final gate: PASS | FAILED after {n} attempts
|
|
|
728
741
|
|
|
729
742
|
_(Omit `## Third-Party Threads` if `COMPLIANCE_SKILL_INSTALLED` is false or no external threads were found.)_
|
|
730
743
|
|
|
731
|
-
**Statistics mapping (parser contract)**: the `Deferred` row = FIX_SEPARATE + TECH_DEBT (both deferral dispositions combined); By Design and Escalated are counted separately and excluded from `Deferred`. The `/code-review` convergence parser reads
|
|
744
|
+
**Statistics mapping (parser contract)**: the `Deferred` row = FIX_SEPARATE + TECH_DEBT (both deferral dispositions combined); By Design and Escalated are counted separately and excluded from `Deferred`. The `Duplicates Collapsed` row is additive — the `/code-review` convergence parser reads only `Deferred`, `Fixed`, and `False Positive` rows plus `## Fixed Issues` / `## False Positives` headings — keep those labels byte-stable. All rows that the parser reads count UNIQUE (non-DUPLICATE) issues only, so collapsed duplicates do not inflate fp\_ratio.
|
package/dist/core/flags.js
CHANGED
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
* point every caller reaches). applyFlags(settingsJson, FlagsRecord) is the
|
|
12
12
|
* sole API; init.ts works directly with FlagsRecord (no legacy string[] bridge).
|
|
13
13
|
*/
|
|
14
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
15
|
+
/**
|
|
16
|
+
* Type predicate that narrows `ClaudeCodeFlag` to `EnvBooleanFlagDef`.
|
|
17
|
+
* Narrows `f.onPayload` to `string` so callers writing to the env string-map
|
|
18
|
+
* avoid an `unknown` hop without an unsafe cast.
|
|
19
|
+
*
|
|
20
|
+
* Use in `buildPayload` and `applyFlags` only where the string type is load-bearing.
|
|
21
|
+
*/
|
|
22
|
+
export function isEnvBooleanFlag(f) {
|
|
23
|
+
return f.kind === 'boolean' && f.target.type === 'env';
|
|
24
|
+
}
|
|
14
25
|
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
15
26
|
// Phase 0 probe findings: see docs/reference/claude-code-flags-probe.md
|
|
16
27
|
/**
|
|
@@ -309,6 +320,24 @@ export const FLAG_REGISTRY = [
|
|
|
309
320
|
recommended: false,
|
|
310
321
|
defaultValue: false,
|
|
311
322
|
},
|
|
323
|
+
{
|
|
324
|
+
// D27: gates Claude Code's AI-attribution injection into git commits and PRs.
|
|
325
|
+
// When true, writes {"commit":"","pr":""} to settings.json which suppresses attribution.
|
|
326
|
+
// When false/null (neutral), removes the key ONLY when the current value is the exact
|
|
327
|
+
// devflow-managed shape (settingDeleteGuard). A custom attribution value (e.g. an org
|
|
328
|
+
// name) is never deleted — shape guard prevents erasure (D-ATTR-GUARD).
|
|
329
|
+
id: 'suppress-attribution',
|
|
330
|
+
label: 'Suppress AI attribution',
|
|
331
|
+
description: 'Remove AI-attribution labels from git commits and pull requests',
|
|
332
|
+
hint: 'Suppresses Claude attribution labels in git commits and pull requests',
|
|
333
|
+
blurb: 'hide AI attribution labels',
|
|
334
|
+
kind: 'boolean',
|
|
335
|
+
target: { type: 'setting', key: 'attribution' },
|
|
336
|
+
onPayload: { commit: '', pr: '' },
|
|
337
|
+
settingDeleteGuard: { commit: '', pr: '' },
|
|
338
|
+
recommended: false,
|
|
339
|
+
defaultValue: false,
|
|
340
|
+
},
|
|
312
341
|
// ── Valued flags (number/enum/string) ────────────────────────────────────
|
|
313
342
|
{
|
|
314
343
|
// Domain: unset by default; set only when users want a non-default spawn depth.
|
|
@@ -837,6 +866,78 @@ export function migrateLegacyFlagsToRecord(enabledIds, knownIds, legacyViewMode)
|
|
|
837
866
|
return result;
|
|
838
867
|
}
|
|
839
868
|
// ─── Apply / Strip ────────────────────────────────────────────────────────────
|
|
869
|
+
/**
|
|
870
|
+
* Returns true when `value` equals the managed shape declared by `flag.settingDeleteGuard`.
|
|
871
|
+
* Returns false when: the flag has no guard, is not a setting-target boolean, or the value
|
|
872
|
+
* does not deep-equal the guard.
|
|
873
|
+
*
|
|
874
|
+
* Single equality oracle for managed-shape comparisons — used by `canDeleteSettingKey`,
|
|
875
|
+
* `settingHoldsManagedShape`, and the Step 2b adoption fold in `convergeFlagsIntoSettings`.
|
|
876
|
+
* All three delegate here so there is exactly one `isDeepStrictEqual` call for guard matching.
|
|
877
|
+
*/
|
|
878
|
+
export function settingValueHoldsManagedShape(flag, value) {
|
|
879
|
+
if (flag.kind !== 'boolean' || flag.target.type !== 'setting')
|
|
880
|
+
return false;
|
|
881
|
+
const guard = flag.settingDeleteGuard;
|
|
882
|
+
if (guard === undefined)
|
|
883
|
+
return false;
|
|
884
|
+
return isDeepStrictEqual(value, guard);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* D-ATTR-GUARD single-source predicate (consistency-01 / ADR-024).
|
|
888
|
+
*
|
|
889
|
+
* Returns true when the settings.json string contains a value at the flag's
|
|
890
|
+
* target key that equals the flag's managed shape (`settingDeleteGuard`).
|
|
891
|
+
*
|
|
892
|
+
* This is the ONE place that decides "on-disk value equals the managed shape".
|
|
893
|
+
* All consumers — `resolveExistingAttributionSuppression` (seeding priority),
|
|
894
|
+
* and the guarded-boolean adoption fold in `convergeFlagsIntoSettings` — delegate
|
|
895
|
+
* here rather than hand-rolling their own comparison.
|
|
896
|
+
*
|
|
897
|
+
* Returns false when:
|
|
898
|
+
* - settingsJson is malformed
|
|
899
|
+
* - root is not a plain object
|
|
900
|
+
* - flagId is not in the registry
|
|
901
|
+
* - the flag has no settingDeleteGuard
|
|
902
|
+
* - the flag is not a setting-target boolean
|
|
903
|
+
* - the on-disk value does not deep-equal the guard
|
|
904
|
+
*/
|
|
905
|
+
export function settingHoldsManagedShape(settingsJson, flagId) {
|
|
906
|
+
try {
|
|
907
|
+
const parsed = JSON.parse(settingsJson);
|
|
908
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
909
|
+
return false;
|
|
910
|
+
const flag = FLAG_REGISTRY_MAP.get(flagId);
|
|
911
|
+
if (!flag)
|
|
912
|
+
return false;
|
|
913
|
+
const value = parsed[flag.target.key];
|
|
914
|
+
return settingValueHoldsManagedShape(flag, value);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* D-ATTR-GUARD: returns true when it is safe to delete a flag's target key from
|
|
922
|
+
* settings. For flags with a settingDeleteGuard the key is only deleted when the
|
|
923
|
+
* current on-disk value deep-equals the managed shape, preventing erasure of
|
|
924
|
+
* user-customized values (e.g. an organisation attribution block) on flag
|
|
925
|
+
* disable or uninstall. Flags without a guard are always safe to delete.
|
|
926
|
+
*
|
|
927
|
+
* Delegates to `settingValueHoldsManagedShape` — the single equality oracle for
|
|
928
|
+
* managed-shape comparisons — so there is exactly one `isDeepStrictEqual` call
|
|
929
|
+
* across the entire apply/strip pipeline (ADR-024 mechanism 3).
|
|
930
|
+
*
|
|
931
|
+
* Single-source invariant: both the disable path (applyFlags neutral branch) and
|
|
932
|
+
* the uninstall path (stripFlags) collapse to this predicate.
|
|
933
|
+
*/
|
|
934
|
+
function canDeleteSettingKey(flag, settings) {
|
|
935
|
+
if (flag.kind !== 'boolean')
|
|
936
|
+
return true;
|
|
937
|
+
if (flag.settingDeleteGuard === undefined)
|
|
938
|
+
return true;
|
|
939
|
+
return settingValueHoldsManagedShape(flag, settings[flag.target.key]);
|
|
940
|
+
}
|
|
840
941
|
/**
|
|
841
942
|
* Return `v` as a `Record<string, unknown>` only when it is a plain object.
|
|
842
943
|
* Returns undefined for arrays, null, or non-objects.
|
|
@@ -853,8 +954,17 @@ function asPlainObject(v) {
|
|
|
853
954
|
/** Compute the value to write to settings.json for an active flag. */
|
|
854
955
|
function buildPayload(flag, value) {
|
|
855
956
|
switch (flag.kind) {
|
|
856
|
-
case 'boolean':
|
|
857
|
-
return
|
|
957
|
+
case 'boolean': {
|
|
958
|
+
// D-PAYLOAD-CLONE: return a structural clone for object payloads so the
|
|
959
|
+
// FLAG_REGISTRY entry is never aliased into the caller's settings tree.
|
|
960
|
+
// Primitives (string, boolean) are values and require no clone.
|
|
961
|
+
// isEnvBooleanFlag narrows onPayload to string for env flags — no clone needed,
|
|
962
|
+
// and the string type avoids an `unknown` hop into the env string-map.
|
|
963
|
+
if (isEnvBooleanFlag(flag))
|
|
964
|
+
return flag.onPayload; // string — no clone needed
|
|
965
|
+
const p = flag.onPayload;
|
|
966
|
+
return typeof p === 'object' && p !== null ? structuredClone(p) : p;
|
|
967
|
+
}
|
|
858
968
|
case 'enum':
|
|
859
969
|
return value;
|
|
860
970
|
case 'number':
|
|
@@ -904,7 +1014,8 @@ export function applyFlags(settingsJson, flags) {
|
|
|
904
1014
|
delete env[flag.target.key];
|
|
905
1015
|
}
|
|
906
1016
|
else {
|
|
907
|
-
|
|
1017
|
+
if (canDeleteSettingKey(flag, settings))
|
|
1018
|
+
delete settings[flag.target.key];
|
|
908
1019
|
}
|
|
909
1020
|
}
|
|
910
1021
|
else {
|
|
@@ -949,7 +1060,8 @@ export function stripFlags(settingsJson) {
|
|
|
949
1060
|
delete env[flag.target.key];
|
|
950
1061
|
}
|
|
951
1062
|
else {
|
|
952
|
-
|
|
1063
|
+
if (canDeleteSettingKey(flag, settings))
|
|
1064
|
+
delete settings[flag.target.key];
|
|
953
1065
|
}
|
|
954
1066
|
}
|
|
955
1067
|
if (env && Object.keys(env).length === 0) {
|
|
@@ -1116,6 +1228,35 @@ export function convergeFlagsIntoSettings(settingsJson, record, opts) {
|
|
|
1116
1228
|
folded[flag.id] = coerced;
|
|
1117
1229
|
}
|
|
1118
1230
|
}
|
|
1231
|
+
// ── Step 2b: adopt guarded boolean settings before the strip ─────────────
|
|
1232
|
+
// D-ATTR-ADOPT (PF-050 / ADR-024): a delete guard is evidence about the VALUE,
|
|
1233
|
+
// never about the record — a pre-existing on-disk key whose value matches the
|
|
1234
|
+
// managed shape means devflow wrote it, so adopt it into the record now, before
|
|
1235
|
+
// stripFlags can unconditionally remove it on the next line.
|
|
1236
|
+
//
|
|
1237
|
+
// Mirror of the view-mode fold (Step 1): adopt only when:
|
|
1238
|
+
// (a) the flag has a settingDeleteGuard (guarded boolean only)
|
|
1239
|
+
// (b) the record does NOT already claim the key — a claimed false or null still
|
|
1240
|
+
// deletes the managed block (the user intentionally disabled it)
|
|
1241
|
+
// (c) the pre-strip on-disk value holds the managed shape
|
|
1242
|
+
//
|
|
1243
|
+
// This fold runs before stripFlags so applyFlags rewrites the block from the
|
|
1244
|
+
// now-claimed record and the block survives on BOTH the init and flags paths.
|
|
1245
|
+
for (const flag of FLAG_REGISTRY) {
|
|
1246
|
+
if (flag.kind !== 'boolean')
|
|
1247
|
+
continue;
|
|
1248
|
+
if (flag.target.type !== 'setting')
|
|
1249
|
+
continue;
|
|
1250
|
+
// Skip when the record already claims this flag (claimed false/null deletes the block)
|
|
1251
|
+
const claimed = claimedIn !== null &&
|
|
1252
|
+
Object.prototype.hasOwnProperty.call(claimedIn, flag.id);
|
|
1253
|
+
if (claimed)
|
|
1254
|
+
continue;
|
|
1255
|
+
// Adopt only when the on-disk value matches the managed shape exactly
|
|
1256
|
+
if (settingValueHoldsManagedShape(flag, parsed[flag.target.key])) {
|
|
1257
|
+
folded[flag.id] = true;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1119
1260
|
// ── Step 3: strip all managed keys, then apply the folded record ──────────
|
|
1120
1261
|
const stripped = stripFlags(settingsJson);
|
|
1121
1262
|
const settings = applyFlags(stripped, folded);
|
package/dist/core/proxy-log.js
CHANGED
|
@@ -29,8 +29,10 @@ export const PROXY_LOG_TAIL_BYTES = 1_048_576;
|
|
|
29
29
|
* (e.g. SUBSWITCH_CONFIG for the relay spawn and doctor spawn).
|
|
30
30
|
*
|
|
31
31
|
* applies ADR-003: the prior denylist rationale is gone — the routing runtime
|
|
32
|
-
* reads exactly
|
|
33
|
-
*
|
|
32
|
+
* reads exactly four env vars (ANTHROPIC_API_KEY, FORCE_COLOR, NO_COLOR,
|
|
33
|
+
* SUBSWITCH_CONFIG — NO_COLOR read in dist/tty.js with presence semantics; CI is
|
|
34
|
+
* read only by the `init` subcommand which devflow never invokes).
|
|
35
|
+
* Verified by whole-dist grep of the 0.4.0 package. An allowlist is the correct
|
|
34
36
|
* shape: 61 inherited vars → 6.
|
|
35
37
|
*
|
|
36
38
|
* HOME is retained: the runtime's loadConfig resolves ~ paths via homedir().
|
package/dist/core/proxy-state.js
CHANGED
|
@@ -88,47 +88,72 @@ export async function writeProxyState(devflowDir, state) {
|
|
|
88
88
|
// Pure helpers
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
/**
|
|
91
|
-
* Accepted top-level keys for the subswitch 0.
|
|
91
|
+
* Accepted top-level keys for the subswitch 0.4.0 FileConfigSchema (z.strictObject).
|
|
92
92
|
* Unknown top-level keys cause a hard relay startup error — never emit them.
|
|
93
93
|
*
|
|
94
|
-
* @D-EFR-4 Subswitch 0.
|
|
94
|
+
* @D-EFR-4 Subswitch 0.4.0 routing config contract:
|
|
95
95
|
* FileConfigSchema is a z.strictObject with exactly 5 accepted top-level keys:
|
|
96
96
|
* port, logLevel, anthropic, providers, limits. Unknown keys cause a hard startup
|
|
97
97
|
* error — the relay refuses to start. anthropic and limits are themselves
|
|
98
98
|
* strictObject + prefault({}), so they may be partially specified.
|
|
99
|
-
*
|
|
100
|
-
* connectTimeoutMs default: subswitch 0.2.0 applies anthropic.connectTimeoutMs
|
|
101
|
-
* as upstream.setTimeout() at anthropic-passthrough.js:86 — a socket *inactivity*
|
|
102
|
-
* timeout, not a connect timeout. The 10s default kills any Anthropic request
|
|
103
|
-
* whose upstream takes >10s to emit the first response byte — routine for long
|
|
104
|
-
* opus requests (confirmed: 99 spurious 504s clustered at 10004-10098ms while
|
|
105
|
-
* successful requests ran 25s-99s). Default overridden to 120 000ms; a
|
|
106
|
-
* user-specified value always wins.
|
|
107
|
-
*
|
|
108
|
-
* limits.connectTimeoutMs is a registered LEGACY KEY that causes a hard startup
|
|
109
|
-
* error. Strip it from the limits block when preserving existing config.
|
|
110
99
|
*/
|
|
111
100
|
const ROUTING_CONFIG_ALLOWED_TOP_KEYS = new Set([
|
|
112
101
|
'port', 'logLevel', 'anthropic', 'providers', 'limits',
|
|
113
102
|
]);
|
|
114
|
-
/**
|
|
115
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Sub-keys of a preserved `anthropic` / `limits` block that the pinned relay rejects
|
|
105
|
+
* outright — each is a registered LEGACY KEY, and a legacy key is a hard startup
|
|
106
|
+
* error naming its replacement, not a warning. They are stripped when carrying a
|
|
107
|
+
* user's existing config forward so an upgrade cannot leave the relay unable to boot.
|
|
108
|
+
*
|
|
109
|
+
* Every entry was valid under a version devflow previously pinned, which is what makes
|
|
110
|
+
* it reachable in a real user's file:
|
|
111
|
+
* anthropic.streamIdleTimeoutMs — valid in 0.2.0, removed in 0.3.0 (relay no longer
|
|
112
|
+
* bounds the stream-idle phase on a connected client).
|
|
113
|
+
* limits.connectTimeoutMs — moved to anthropic.connectTimeoutMs in 0.3.0;
|
|
114
|
+
* stripping it loses nothing (relay default governs).
|
|
115
|
+
* limits.maxConcurrentRequests — valid in 0.2.0, removed in 0.3.0 (admission gate
|
|
116
|
+
* removed).
|
|
117
|
+
* limits.maxBodyBytes — valid through 0.3.0, renamed to
|
|
118
|
+
* limits.maxBufferedBodyBytes in 0.4.0. The old spelling is a registered legacy
|
|
119
|
+
* key, so leaving it in place would stop the relay from booting.
|
|
120
|
+
* limits.maxUpstreamSockets — valid in 0.3.0, moved to anthropic.maxUpstreamSockets
|
|
121
|
+
* in 0.4.0's providers.* restructure.
|
|
122
|
+
* limits.streamIdleTimeoutMs — valid in 0.3.0, moved to
|
|
123
|
+
* providers.codex.streamIdleTimeoutMs in 0.4.0.
|
|
124
|
+
* limits.requestTimeoutMs — valid in 0.3.0, moved to
|
|
125
|
+
* providers.codex.requestTimeoutMs in 0.4.0.
|
|
126
|
+
* limits.maxSseEventBytes — valid in 0.3.0, moved to
|
|
127
|
+
* providers.codex.maxSseEventBytes in 0.4.0.
|
|
128
|
+
*
|
|
129
|
+
* Keys retired before the 0.2.0 baseline are deliberately absent: only keys reachable
|
|
130
|
+
* in a config written by a previously pinned devflow version belong on this list.
|
|
131
|
+
*/
|
|
132
|
+
const ROUTING_CONFIG_REJECTED_SUBKEYS = {
|
|
133
|
+
anthropic: ['streamIdleTimeoutMs'],
|
|
134
|
+
limits: [
|
|
135
|
+
'connectTimeoutMs',
|
|
136
|
+
'maxConcurrentRequests',
|
|
137
|
+
'maxBodyBytes',
|
|
138
|
+
'maxUpstreamSockets',
|
|
139
|
+
'streamIdleTimeoutMs',
|
|
140
|
+
'requestTimeoutMs',
|
|
141
|
+
'maxSseEventBytes',
|
|
142
|
+
],
|
|
143
|
+
};
|
|
116
144
|
/**
|
|
117
145
|
* Build the routing config JSON for ~/.devflow/proxy-routing.json.
|
|
118
146
|
*
|
|
119
147
|
* Authoritatively sets `port`. Preserves any existing valid `anthropic`,
|
|
120
|
-
* `limits`, `logLevel`, and `providers` blocks from `existingContent`,
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* Injects a default `anthropic.connectTimeoutMs` of 120 000ms when the user
|
|
125
|
-
* has not set one — see @D-EFR-4 for the rationale.
|
|
148
|
+
* `limits`, `logLevel`, and `providers` blocks from `existingContent`, filtering
|
|
149
|
+
* out unknown top-level keys and every sub-key in ROUTING_CONFIG_REJECTED_SUBKEYS
|
|
150
|
+
* (each one a hard relay startup error). No `anthropic` block is injected when
|
|
151
|
+
* the user has not set one — the relay's own default governs (D-EFR-4).
|
|
126
152
|
*
|
|
127
|
-
* If `existingContent` is missing or malformed, falls back to a
|
|
128
|
-
* with only `port` and `anthropic.connectTimeoutMs`.
|
|
153
|
+
* If `existingContent` is missing or malformed, falls back to a port-only config.
|
|
129
154
|
*
|
|
130
155
|
* applies ADR-013: pure core-layer function — no I/O.
|
|
131
|
-
* @D-EFR-4: see note above for the strict top-key constraint
|
|
156
|
+
* @D-EFR-4: see note above for the strict top-key constraint.
|
|
132
157
|
*/
|
|
133
158
|
export function buildRoutingConfigJson(port, existingContent) {
|
|
134
159
|
// Parse existing config tolerantly; a malformed file falls back to clean defaults.
|
|
@@ -154,30 +179,36 @@ export function buildRoutingConfigJson(port, existingContent) {
|
|
|
154
179
|
if (preserved.logLevel !== undefined) {
|
|
155
180
|
config.logLevel = preserved.logLevel;
|
|
156
181
|
}
|
|
157
|
-
// Anthropic block: preserve user settings;
|
|
158
|
-
//
|
|
159
|
-
|
|
182
|
+
// Anthropic block: preserve user settings; strip legacy rejected sub-keys.
|
|
183
|
+
// No default is injected — the relay's own default governs (D-EFR-4).
|
|
184
|
+
if (typeof preserved.anthropic === 'object' &&
|
|
160
185
|
preserved.anthropic !== null &&
|
|
161
|
-
!Array.isArray(preserved.anthropic)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
!Array.isArray(preserved.anthropic)) {
|
|
187
|
+
const existingAnthropic = { ...preserved.anthropic };
|
|
188
|
+
for (const key of ROUTING_CONFIG_REJECTED_SUBKEYS.anthropic) {
|
|
189
|
+
delete existingAnthropic[key];
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(existingAnthropic).length > 0) {
|
|
192
|
+
config.anthropic = existingAnthropic;
|
|
193
|
+
}
|
|
166
194
|
}
|
|
167
|
-
config.anthropic = existingAnthropic;
|
|
168
195
|
// Preserve providers block if present.
|
|
169
196
|
if (preserved.providers !== undefined) {
|
|
170
197
|
config.providers = preserved.providers;
|
|
171
198
|
}
|
|
172
|
-
// Preserve limits block if present
|
|
173
|
-
//
|
|
199
|
+
// Preserve limits block if present, minus the sub-keys the relay hard-errors on
|
|
200
|
+
// (see ROUTING_CONFIG_REJECTED_SUBKEYS).
|
|
174
201
|
if (preserved.limits !== undefined) {
|
|
175
202
|
if (typeof preserved.limits === 'object' &&
|
|
176
203
|
preserved.limits !== null &&
|
|
177
204
|
!Array.isArray(preserved.limits)) {
|
|
178
205
|
const limitsObj = { ...preserved.limits };
|
|
179
|
-
|
|
180
|
-
|
|
206
|
+
for (const key of ROUTING_CONFIG_REJECTED_SUBKEYS.limits) {
|
|
207
|
+
delete limitsObj[key];
|
|
208
|
+
}
|
|
209
|
+
if (Object.keys(limitsObj).length > 0) {
|
|
210
|
+
config.limits = limitsObj;
|
|
211
|
+
}
|
|
181
212
|
}
|
|
182
213
|
else {
|
|
183
214
|
config.limits = preserved.limits;
|
|
@@ -210,6 +241,29 @@ export function proxyBaseUrl(port) {
|
|
|
210
241
|
// ---------------------------------------------------------------------------
|
|
211
242
|
// isProxyEnabled — the primary contract other modules consume
|
|
212
243
|
// ---------------------------------------------------------------------------
|
|
244
|
+
/**
|
|
245
|
+
* Returns true when ~/.devflow/proxy.json exists on disk — i.e. Devflow has
|
|
246
|
+
* previously managed the proxy on this machine.
|
|
247
|
+
*
|
|
248
|
+
* This is the evidence gate used by init and uninstall before stripping
|
|
249
|
+
* ANTHROPIC_BASE_URL / CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT:
|
|
250
|
+
* we only strip those vars when we can prove Devflow wrote them.
|
|
251
|
+
*
|
|
252
|
+
* D-STRIP-1: gate proxy env stripping on devflow-managed evidence.
|
|
253
|
+
* readProxyState() returns Ok(defaultState) on ENOENT — it cannot distinguish
|
|
254
|
+
* "file absent" from "file present with DEFAULT_PROXY_PORT". Callers that need
|
|
255
|
+
* to differentiate must use proxyJsonExists() rather than checking the result
|
|
256
|
+
* of readProxyState().
|
|
257
|
+
*/
|
|
258
|
+
export async function proxyJsonExists(devflowDir) {
|
|
259
|
+
try {
|
|
260
|
+
await fs.access(join(devflowDir, 'proxy.json'));
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
213
267
|
/**
|
|
214
268
|
* Check whether the Devflow proxy is currently enabled.
|
|
215
269
|
* Returns false when the proxy state file is missing, unreadable, or malformed.
|