devflow-kit 3.4.0 → 3.5.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 +29 -0
- package/README.md +8 -4
- package/dist/agents/code-edit.md +133 -0
- package/dist/agents/code-fix.md +165 -0
- package/dist/agents/code.md +39 -182
- package/dist/agents/design.md +0 -1
- package/dist/agents/git.md +31 -693
- package/dist/agents/knowledge.md +8 -2
- package/dist/agents/research.md +6 -0
- package/dist/agents/review.md +0 -4
- package/dist/agents/scrutinize.md +3 -0
- package/dist/agents/skim.md +4 -0
- package/dist/agents/triage.md +8 -1
- package/dist/cli/commands/behavior-prompts.js +77 -0
- package/dist/cli/commands/behavior.js +123 -0
- package/dist/cli/commands/flags.js +8 -2
- package/dist/cli/commands/init-seed.js +7 -3
- package/dist/cli/commands/init.js +72 -5
- package/dist/cli/commands/rules.js +16 -0
- package/dist/cli/commands/tracker.js +112 -15
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +5 -5
- package/dist/commands/code-review.md +14 -9
- package/dist/commands/debug.md +25 -4
- package/dist/commands/dynamic-build.md +28 -38
- package/dist/commands/dynamic-plan.md +11 -9
- package/dist/commands/dynamic-profile.md +3 -3
- package/dist/commands/dynamic-tickets.md +12 -10
- package/dist/commands/explore.md +3 -3
- package/dist/commands/implement.md +91 -125
- package/dist/commands/plan.md +15 -8
- package/dist/commands/release.md +3 -2
- package/dist/commands/research.md +16 -9
- package/dist/commands/resolve.md +20 -15
- package/dist/commands/self-review.md +2 -2
- package/dist/core/agent-frontmatter.js +101 -0
- package/dist/core/compliance-compose.js +6 -4
- package/dist/core/feature-switch.js +23 -2
- package/dist/core/flags.js +111 -0
- package/dist/core/manifest.js +11 -3
- package/dist/core/mds-variants.js +68 -26
- package/dist/core/plugins.js +42 -65
- package/dist/core/project-paths.js +0 -4
- package/dist/core/proxy-log.js +1 -1
- package/dist/core/proxy-state.js +1 -1
- package/dist/core/tracker.js +120 -2
- package/dist/learning-off/agents/code-edit.md +129 -0
- package/dist/learning-off/agents/code-fix.md +161 -0
- package/dist/learning-off/agents/code.md +37 -180
- package/dist/learning-off/agents/design.md +0 -1
- package/dist/learning-off/agents/knowledge.md +8 -2
- package/dist/learning-off/agents/research.md +6 -0
- package/dist/learning-off/agents/review.md +0 -4
- package/dist/learning-off/agents/scrutinize.md +3 -0
- package/dist/learning-off/agents/skim.md +4 -0
- package/dist/learning-off/agents/triage.md +8 -1
- package/dist/learning-off/commands/bug-analysis.md +3 -3
- package/dist/learning-off/commands/code-review.md +12 -7
- package/dist/learning-off/commands/debug.md +22 -2
- package/dist/learning-off/commands/dynamic-build.md +27 -37
- package/dist/learning-off/commands/dynamic-plan.md +10 -8
- package/dist/learning-off/commands/dynamic-profile.md +2 -2
- package/dist/learning-off/commands/dynamic-tickets.md +11 -9
- package/dist/learning-off/commands/explore.md +1 -1
- package/dist/learning-off/commands/implement.md +89 -123
- package/dist/learning-off/commands/plan.md +13 -6
- package/dist/learning-off/commands/release.md +3 -2
- package/dist/learning-off/commands/research.md +14 -7
- package/dist/learning-off/commands/resolve.md +18 -13
- package/dist/skills/git/references/learn-conventions.md +27 -3
- package/dist/skills/git/references/pr/check-ci-status.md +19 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +19 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
- package/dist/skills/git/references/pr/post-review-summary.md +20 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
- package/dist/skills/git/references/pr/update-pr-evidence.md +13 -0
- package/dist/skills/git/references/pr/validate-branch.md +26 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/github/create-release.md +30 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/github/fetch-issue.md +36 -1
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/github/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/github/setup-task.md +41 -1
- package/dist/skills/git/references/tracker/jira/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +36 -1
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +41 -1
- package/dist/skills/git/references/tracker/linear/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +36 -1
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +41 -1
- package/dist/targets/claude-code/agent-type-owner.js +164 -0
- package/dist/targets/claude-code/behavior-install.js +111 -0
- package/dist/targets/claude-code/installer.js +69 -2
- package/dist/targets/claude-code/learning-install.js +9 -1
- package/dist/targets/claude-code/tracker-install.js +33 -16
- package/dist/targets/claude-code/tracker-servers.js +107 -0
- package/package.json +4 -3
- package/src/assets/agents/_partials/_code.mds +109 -0
- package/src/assets/agents/code-edit.mds +81 -0
- package/src/assets/agents/code-fix.mds +113 -0
- package/src/assets/agents/code.mds +18 -222
- package/src/assets/agents/design.mds +0 -1
- package/src/assets/agents/evaluate.md +7 -1
- package/src/assets/agents/explore.md +58 -0
- package/src/assets/agents/git.mds +31 -693
- package/src/assets/agents/knowledge.mds +8 -2
- package/src/assets/agents/learning.md +19 -69
- package/src/assets/agents/research.mds +6 -0
- package/src/assets/agents/review.mds +0 -4
- package/src/assets/agents/scrutinize.mds +3 -0
- package/src/assets/agents/simplify.md +3 -0
- package/src/assets/agents/skim.mds +4 -0
- package/src/assets/agents/synthesize.md +26 -352
- package/src/assets/agents/test.md +8 -0
- package/src/assets/agents/tracker.md +20 -7
- package/src/assets/agents/triage.mds +8 -1
- package/src/assets/agents/validate.md +5 -0
- package/src/assets/commands/_partials/_compliance.mds +36 -0
- package/src/assets/commands/_partials/_decisions.mds +2 -2
- package/src/assets/commands/_partials/_engine.mds +8 -8
- package/src/assets/commands/_partials/_preamble.mds +3 -3
- package/src/assets/commands/_partials/_roster.mds +4 -3
- package/src/assets/commands/_partials/_wave.mds +2 -2
- package/src/assets/commands/bug-analysis.mds +3 -3
- package/src/assets/commands/code-review.mds +11 -8
- package/src/assets/commands/debug.mds +24 -2
- package/src/assets/commands/dynamic-build.mds +14 -25
- package/src/assets/commands/dynamic-plan.mds +4 -3
- package/src/assets/commands/dynamic-tickets.mds +5 -4
- package/src/assets/commands/explore.mds +1 -1
- package/src/assets/commands/implement.mds +89 -125
- package/src/assets/commands/plan.mds +12 -7
- package/src/assets/commands/release.mds +3 -2
- package/src/assets/commands/research.mds +14 -7
- package/src/assets/commands/resolve.mds +17 -14
- package/src/assets/mds/.mdsroot +4 -0
- package/src/assets/mds/git/_ops.mds +383 -0
- package/src/assets/mds/git/_ops_pr.mds +201 -0
- package/src/assets/mds/git/_ops_steps.mds +147 -0
- package/src/assets/mds/git/_pr.mds +39 -44
- package/src/assets/mds/git/_references.mds +6 -3
- package/src/assets/mds/tracker/_common.mds +5 -5
- package/src/assets/mds/tracker/_contract.mds +4 -3
- package/src/assets/mds/tracker/_github.mds +60 -14
- package/src/assets/mds/tracker/_jira.mds +60 -14
- package/src/assets/mds/tracker/_linear.mds +60 -14
- package/src/assets/mds/tracker/_steps.mds +10 -9
- package/src/assets/rules/behavior.md +6 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +2 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -6
- package/src/assets/skills/agent-references/SKILL.md +33 -0
- package/src/assets/skills/agent-references/references/learning/maintain.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
- package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
- package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
- package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
- package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +80 -83
- package/src/assets/skills/docs-framework/references/patterns.md +14 -25
- package/src/assets/skills/docs-framework/references/violations.md +7 -7
- package/src/assets/skills/gap-analysis/SKILL.md +0 -16
- package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
|
@@ -78,9 +78,9 @@ Attempt to read `{ledger}/.devflow/learning/index.md`.
|
|
|
78
78
|
|
|
79
79
|
The index is one direct file read, written at render time by `render-decisions.cjs` alongside `decisions.md`/`pitfalls.md` — no `.cjs` script runs here, and the index's own footer names the files that hold each entry's full body.
|
|
80
80
|
|
|
81
|
-
**Step 2 —
|
|
81
|
+
**Step 2 — Agents apply the decisions:**
|
|
82
82
|
|
|
83
|
-
When `DECISIONS_CONTEXT` is not `(none)`, follow `devflow:apply-decisions
|
|
83
|
+
When `DECISIONS_CONTEXT` is not `(none)`, hand it only to the agents whose contract declares it. They follow `devflow:apply-decisions`: scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in their reasoning. A host that uses the index in its own thread says so in its own step.
|
|
84
84
|
|
|
85
85
|
Use `DECISIONS_CONTEXT` locally when framing research — prior decisions and pitfalls suggest areas to investigate. Follow `devflow:apply-decisions` to Read full entry bodies on demand. Pass `DECISIONS_CONTEXT` to each Research agent in Phase 4 so their findings can account for relevant decisions.
|
|
86
86
|
|
|
@@ -145,6 +145,10 @@ Use `FEATURE_KNOWLEDGE` **locally** for research framing. Pass to each Research
|
|
|
145
145
|
|
|
146
146
|
**Produces:** RESEARCH_PLAN
|
|
147
147
|
|
|
148
|
+
**Load `devflow:docs-framework`** — Load via Skill tool: `devflow:docs-framework`, here and not earlier, so a run that stops before this phase never pays for it. If it fails to load, continue with the rules below.
|
|
149
|
+
|
|
150
|
+
Name the research directory by the skill's rules, once: `{topic-slug}` is its topic slug of the research question and `{YYYY-MM-DD_HHMM}` is the local time. When `{worktree}/.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}` already exists (a same-minute collision), the timestamp takes seconds instead, `YYYY-MM-DD_HHMMSS`. Every path of this run uses that one directory.
|
|
151
|
+
|
|
148
152
|
Analyze the research question to infer research types needed (min 2, max 5). For each type:
|
|
149
153
|
- `RESEARCH_TYPE`: `codebase | external | market | competitor | technology`
|
|
150
154
|
- `RESEARCH_QUESTION`: Focused sub-question for this type
|
|
@@ -152,8 +156,6 @@ Analyze the research question to infer research types needed (min 2, max 5). For
|
|
|
152
156
|
|
|
153
157
|
**Tool availability check**: If WebSearch/WebFetch are unavailable, restrict to `codebase` type only.
|
|
154
158
|
|
|
155
|
-
Generate topic slug from the research question (kebab-case, lowercase, no articles).
|
|
156
|
-
|
|
157
159
|
### Phase 3: Orient (Conditional)
|
|
158
160
|
|
|
159
161
|
**Produces:** ORIENT_OUTPUT
|
|
@@ -185,12 +187,17 @@ Each Research agent writes findings to disk at OUTPUT_PATH.
|
|
|
185
187
|
**Produces:** RESEARCH_SUMMARY
|
|
186
188
|
**Requires:** RESEARCH_OUTPUTS
|
|
187
189
|
|
|
188
|
-
Spawn `Agent(subagent_type="Synthesize")` in `research` mode:
|
|
189
|
-
- Reads Research agent outputs from RESEARCH_OUTPUTS paths
|
|
190
|
-
- Merges findings with trust-aware aggregation
|
|
191
|
-
- Writes `research-summary.md` to the same timestamped directory
|
|
190
|
+
Spawn `Agent(subagent_type="Synthesize")` in `research` mode. It reads the Research agent outputs from the `RESEARCH_OUTPUTS` paths, merges them with trust-aware aggregation, and writes `research-summary.md` into `RESEARCH_BASE_DIR` — the one directory of this run, which only this spawn names:
|
|
192
191
|
|
|
193
|
-
|
|
192
|
+
```
|
|
193
|
+
Agent(subagent_type="Synthesize", run_in_background=false):
|
|
194
|
+
"Mode: research
|
|
195
|
+
WORKTREE_PATH: {WORKTREE_PATH} (omit if none was provided)
|
|
196
|
+
RESEARCH_OUTPUTS: {the OUTPUT_PATH of every Research agent that wrote its file}
|
|
197
|
+
RESEARCH_BASE_DIR: {worktree}/.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}
|
|
198
|
+
Merge findings with trust-aware aggregation.
|
|
199
|
+
Output: {worktree}/.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}/research-summary.md"
|
|
200
|
+
```
|
|
194
201
|
|
|
195
202
|
### Phase 6: Present
|
|
196
203
|
|
package/dist/commands/resolve.md
CHANGED
|
@@ -22,9 +22,10 @@ Process issues from code review reports: triage every issue through the blast-ra
|
|
|
22
22
|
|
|
23
23
|
**Produces:** WORKTREES
|
|
24
24
|
|
|
25
|
-
1. **Discover resolvable worktrees
|
|
26
|
-
- Run `git worktree list --porcelain`
|
|
27
|
-
- Invoke the `devflow:worktree-support` skill, then Read its `references/discovery.md` from the skill's base directory for the full 7-step algorithm; the canonical protected branch list stays in the skill
|
|
25
|
+
1. **Discover resolvable worktrees**:
|
|
26
|
+
- Run `git worktree list --porcelain` and count the worktrees it lists
|
|
27
|
+
- **More than one listed:** Invoke the `devflow:worktree-support` skill, then Read its `references/discovery.md` from the skill's base directory for the full 7-step algorithm; the canonical protected branch list stays in the skill. Parse the list, filter (skip protected/detached/mid-rebase), dedup by branch, sort by recent commit
|
|
28
|
+
- **Exactly one listed:** load neither the skill nor the reference — that worktree is the only candidate, and Step 0b's Git agent validates its branch
|
|
28
29
|
- Additional filter: must have unresolved reviews (latest review directory has no `resolution-summary.md`)
|
|
29
30
|
2. **If `--path` flag provided:** use only that worktree, skip discovery
|
|
30
31
|
**`--path` validation**: Before proceeding, verify the path exists as a directory and appears in `git worktree list` output. If not: report error and stop.
|
|
@@ -124,9 +125,9 @@ Attempt to read `{ledger}/.devflow/learning/index.md`.
|
|
|
124
125
|
|
|
125
126
|
The index is one direct file read, written at render time by `render-decisions.cjs` alongside `decisions.md`/`pitfalls.md` — no `.cjs` script runs here, and the index's own footer names the files that hold each entry's full body.
|
|
126
127
|
|
|
127
|
-
**Step 2 —
|
|
128
|
+
**Step 2 — Agents apply the decisions:**
|
|
128
129
|
|
|
129
|
-
When `DECISIONS_CONTEXT` is not `(none)`, follow `devflow:apply-decisions
|
|
130
|
+
When `DECISIONS_CONTEXT` is not `(none)`, hand it only to the agents whose contract declares it. They follow `devflow:apply-decisions`: scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in their reasoning. A host that uses the index in its own thread says so in its own step.
|
|
130
131
|
|
|
131
132
|
This produces a compact index of active ADR/PF entries written at render time — Deprecated/Superseded entries are already stripped. Pass `DECISIONS_CONTEXT` to the Triage agent in Phase 2 and to Code agents in Phase 4.
|
|
132
133
|
|
|
@@ -205,6 +206,12 @@ Reuse this result for every worktree.
|
|
|
205
206
|
Note: `auto` is NOT fail-open — under `auto`, the Git agent probes the repository visibility and treats any error or unrecognised value as PUBLIC (mode STUB). What each value does is decided by the Git agent's publication gate (`references/publication-gate.md` step 2); this partial only resolves the value.
|
|
206
207
|
Resolve this per worktree — unlike the evidence policy above, the settings line is per worktree root. From the same line: `COMPLIANCE_FRAMEWORKS` is the settings line's `COMPLIANCE` with `generic` written `none`: `off`, `none`, or the framework ids the machine and this repository declare.
|
|
207
208
|
|
|
209
|
+
**Set the compliance directive** from `COMPLIANCE_FRAMEWORKS`. `COMPLIANCE_DIRECTIVE` is empty when `COMPLIANCE_FRAMEWORKS` is `off`: a spawn line that holds only `{COMPLIANCE_DIRECTIVE}` is then left out, so a lens-off spawn carries nothing about compliance. Otherwise `COMPLIANCE_DIRECTIVE` is this one line, with `<ids>` replaced by the value of `COMPLIANCE_FRAMEWORKS`:
|
|
210
|
+
|
|
211
|
+
> When the task touches regulated surface (data models, auth flows, logging/observability, payments, IaC, retention), invoke Skill(skill="devflow:compliance") and load references/{id}.md only for the ids in <ids> (none: generic controls only); never fabricate guidance for a framework you were not given.
|
|
212
|
+
|
|
213
|
+
Set it per worktree, from that worktree's lens, and pass that worktree's `COMPLIANCE_DIRECTIVE` to each of its Code and Code-fix spawns, on a line of its own.
|
|
214
|
+
|
|
208
215
|
**Mint `RESOLUTION_TS` once per run:** `date -u +%Y-%m-%dT%H:%M:%SZ`. Every worktree reuses it — the resolution comment's dedup marker is searched per PR, so one run key keeps a retry of this run deduplicated while the next /resolve on the same PR posts. Phase 5 writes it as the summary's `**Date**:` and Phase 9b-2 passes it, so the local file and the posted marker agree.
|
|
209
216
|
|
|
210
217
|
### Phase 1: Parse Issues
|
|
@@ -319,11 +326,10 @@ TASK_ID: resolve-{batch-id}
|
|
|
319
326
|
ISSUES: {fix_now_issues_in_batch}
|
|
320
327
|
SCOPE: {risk_tier_per_issue}
|
|
321
328
|
PUSH: false
|
|
322
|
-
CREATE_PR: false
|
|
323
329
|
WORKTREE_PATH: {worktree_path} (omit if cwd)
|
|
324
330
|
DECISIONS_CONTEXT: {decisions_context}
|
|
325
331
|
FEATURE_KNOWLEDGE: {feature_knowledge}
|
|
326
|
-
|
|
332
|
+
{COMPLIANCE_DIRECTIVE}
|
|
327
333
|
Fix only the listed pre-classified FIX_NOW issues. Do not re-litigate dispositions.
|
|
328
334
|
Same-file issues → one commit. Include ## Verification block in report.
|
|
329
335
|
Regression fix without failing-then-passing regression test → BLOCKED."
|
|
@@ -386,18 +392,17 @@ Run build, typecheck, lint, test. Report pass/fail with failure details."
|
|
|
386
392
|
1. Extract failure details from Validate agent output
|
|
387
393
|
2. Increment `validation_retry_count`
|
|
388
394
|
3. If `validation_retry_count <= 2`:
|
|
389
|
-
- Spawn Code agent with fix context:
|
|
395
|
+
- Spawn Code-fix agent with fix context:
|
|
390
396
|
```
|
|
391
|
-
Agent(subagent_type="Code"):
|
|
397
|
+
Agent(subagent_type="Code-fix"):
|
|
392
398
|
"OPERATION: validation-fix
|
|
393
399
|
TASK_ID: resolve-{batch-id}-valfix-{count}
|
|
394
400
|
VALIDATION_FAILURES: {parsed failures from Validate agent}
|
|
395
401
|
SCOPE: Fix only the listed failures, no other changes
|
|
396
402
|
PUSH: false
|
|
397
|
-
CREATE_PR: false
|
|
398
403
|
WORKTREE_PATH: {worktree_path} (omit if cwd)
|
|
399
404
|
DECISIONS_CONTEXT: {decisions_context}
|
|
400
|
-
|
|
405
|
+
{COMPLIANCE_DIRECTIVE}"
|
|
401
406
|
```
|
|
402
407
|
- Loop back to re-validate
|
|
403
408
|
4. If `validation_retry_count > 2`: set VERIFICATION_STATUS = FAILED and exit the validation loop — record a blocking callout in the `## Verification` section (never a silent pass).
|
|
@@ -407,7 +412,7 @@ Run build, typecheck, lint, test. Report pass/fail with failure details."
|
|
|
407
412
|
Record `## Verification` outcome in resolution-summary.md (runs unconditionally after the validation loop exits):
|
|
408
413
|
- Rewrite the `## Verification` section with | Command | Result | table + `Final gate: PASS | FAILED after N attempts`
|
|
409
414
|
|
|
410
|
-
Push all commits to remote once (Code
|
|
415
|
+
Push all commits to remote once (Code and Code-fix agents ran with PUSH: false — runs whether the gate PASSED or FAILED, so the branch is always visible before Phase 9 or CI runs):
|
|
411
416
|
```bash
|
|
412
417
|
git -C {worktree} push
|
|
413
418
|
```
|
|
@@ -445,7 +450,7 @@ Phase 7 has pushed already, so this is a no-op unless the head moved since. `exi
|
|
|
445
450
|
3. **If NO_PR or NO_CI** → skip: "No PR/CI configured, skipping CI validation." Proceed to next phase.
|
|
446
451
|
4. **If PENDING** and fewer than 3 waits have run → wait again (step 1). After the third wait → report "CI still running — verify manually before merging" and proceed.
|
|
447
452
|
5. **If INDETERMINATE** and fewer than 3 waits have run → wait again (step 1). After the third wait → report "CI status unknown — verify manually before merging" and proceed.
|
|
448
|
-
6. **If FAILING** and fewer than 2 fixes have run → report the failing checks from the line. Spawn `Agent(subagent_type="Code")` whose prompt opens with `OPERATION: ci-fix`, with `
|
|
453
|
+
6. **If FAILING** and fewer than 2 fixes have run → report the failing checks from the line. Spawn `Agent(subagent_type="Code-fix")` whose prompt opens with `OPERATION: ci-fix`, with `COMPLIANCE_DIRECTIVE` (when it is not empty), `CI_FAILURES`, `DECISIONS_CONTEXT` and `PUSH: false`; `CI_FAILURES` holds the failing-check names from the line and nothing else, because the Code-fix agent fetches the full names and reads the logs itself and this command reads none. After a fix, push with the command above and, if a wait remains, wait again (step 1); a failed push records `TRACEABILITY: DEGRADED (ci push failed)`, reports "CI status unknown — verify manually before merging" and stops waiting. After the second fix still FAILING → report the failing checks and proceed.
|
|
449
454
|
7. **Budget**: at most 3 waits and 2 fixes in all, per worktree. When one is spent, report the current status and proceed.
|
|
450
455
|
<!-- /PATTERN: ci-status-gate -->
|
|
451
456
|
|
|
@@ -729,10 +734,10 @@ When the Knowledge agent reports `KB_COMMIT: skipped (detached HEAD)`, the files
|
|
|
729
734
|
│
|
|
730
735
|
├─ Phase 6: Simplify [Simplify agent]
|
|
731
736
|
│
|
|
732
|
-
├─ Phase 7: Verification Gate [Validate agent, haiku] + push + Code agent validation-fix loop ≤2
|
|
737
|
+
├─ Phase 7: Verification Gate [Validate agent, haiku] + push + Code-fix agent validation-fix loop ≤2
|
|
733
738
|
│
|
|
734
739
|
├─ Phase 8: CI Status Gate (conditional — skipped if no fixes or verification FAILED)
|
|
735
|
-
│ └─ push, then ci-wait.cjs (inline, one wait ≤ 570 s) → Code agent on FAILING (3 waits + 2 fixes per worktree)
|
|
740
|
+
│ └─ push, then ci-wait.cjs (inline, one wait ≤ 570 s) → Code-fix agent on FAILING (3 waits + 2 fixes per worktree)
|
|
736
741
|
│
|
|
737
742
|
├─ Phase 9: Git agent (manage-debt) — FIX_SEPARATE + TECH_DEBT → backfill Tracked={ISSUE_REF} (or TRACEABILITY: DEGRADED on failure)
|
|
738
743
|
│ SEQUENTIAL across worktrees
|
|
@@ -61,9 +61,9 @@ Attempt to read `{ledger}/.devflow/learning/index.md`.
|
|
|
61
61
|
|
|
62
62
|
The index is one direct file read, written at render time by `render-decisions.cjs` alongside `decisions.md`/`pitfalls.md` — no `.cjs` script runs here, and the index's own footer names the files that hold each entry's full body.
|
|
63
63
|
|
|
64
|
-
**Step 2 —
|
|
64
|
+
**Step 2 — Agents apply the decisions:**
|
|
65
65
|
|
|
66
|
-
When `DECISIONS_CONTEXT` is not `(none)`, follow `devflow:apply-decisions
|
|
66
|
+
When `DECISIONS_CONTEXT` is not `(none)`, hand it only to the agents whose contract declares it. They follow `devflow:apply-decisions`: scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in their reasoning. A host that uses the index in its own thread says so in its own step.
|
|
67
67
|
|
|
68
68
|
Pass `DECISIONS_CONTEXT` to Scrutinize agent — the compact index lists active ADR/PF entries; Scrutinize agent uses `devflow:apply-decisions` to Read full entry bodies on demand. Known pitfalls help identify reintroduced issues, prior decisions help validate architectural consistency. (Simplify agent does not consume decisions — it operates at code-shape level and Scrutinize agent runs after to catch any architectural drift.)
|
|
69
69
|
|
|
@@ -231,4 +231,105 @@ export function rewriteAgentFrontmatter(content, opts) {
|
|
|
231
231
|
const changed = newContent !== content;
|
|
232
232
|
return Ok({ content: newContent, changed });
|
|
233
233
|
}
|
|
234
|
+
const WHOLE_SERVER_PREFIX = 'mcp__';
|
|
235
|
+
/** A server name as it appears in a whole-server entry: the charset Claude Code keeps, no `__`. */
|
|
236
|
+
const WHOLE_SERVER_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
237
|
+
/**
|
|
238
|
+
* True when `tool` is a whole-server allowlist entry: `mcp__` and a server name of
|
|
239
|
+
* 1-64 characters from `[A-Za-z0-9_-]` with no `__` (D-WHOLE-SERVER-ENTRY).
|
|
240
|
+
*/
|
|
241
|
+
export function isWholeServerTool(tool) {
|
|
242
|
+
if (!tool.startsWith(WHOLE_SERVER_PREFIX))
|
|
243
|
+
return false;
|
|
244
|
+
const name = tool.slice(WHOLE_SERVER_PREFIX.length);
|
|
245
|
+
return WHOLE_SERVER_NAME_RE.test(name) && !name.includes('__');
|
|
246
|
+
}
|
|
247
|
+
/** The single-line inline form `tools: ["A", "B"]`, within a frontmatter body. */
|
|
248
|
+
const INLINE_TOOLS_RE = /^tools:[ \t]*\[([^\r\n]*)\][ \t]*(?=\r?$)/m;
|
|
249
|
+
function toolItems(inner) {
|
|
250
|
+
return inner.split(',').filter(item => item.trim() !== '');
|
|
251
|
+
}
|
|
252
|
+
function unquoteTool(item) {
|
|
253
|
+
return item.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Replace the whole-server entries of the first frontmatter block's inline `tools:` line
|
|
257
|
+
* with `extraTools`, and touch nothing else.
|
|
258
|
+
*
|
|
259
|
+
* Idempotent: stamping the same list twice yields the same bytes, and stamping a new list
|
|
260
|
+
* replaces the previous one rather than adding to it. Every other entry of the line keeps
|
|
261
|
+
* its original text, and every other byte of the file (other keys, the body, CRLF or LF)
|
|
262
|
+
* is untouched. An empty `extraTools` removes the stamp.
|
|
263
|
+
*
|
|
264
|
+
* Rejects, rather than repairs: an entry that is not a whole-server entry (`invalid-tool`;
|
|
265
|
+
* it would otherwise reach the frontmatter as free text), a file with no inline
|
|
266
|
+
* `tools: [...]` line (`no-tools-line`; a block-list `tools:` is not edited) and a file
|
|
267
|
+
* with no well-formed frontmatter.
|
|
268
|
+
*
|
|
269
|
+
* Pure function — no I/O.
|
|
270
|
+
*/
|
|
271
|
+
export function stampAgentTools(content, extraTools) {
|
|
272
|
+
for (const tool of extraTools) {
|
|
273
|
+
if (!isWholeServerTool(tool))
|
|
274
|
+
return Err('invalid-tool');
|
|
275
|
+
}
|
|
276
|
+
const parts = parseFrontmatter(content);
|
|
277
|
+
if (!parts.ok)
|
|
278
|
+
return Err(parts.error);
|
|
279
|
+
const { fmBody, openDelim, closeDelim, afterClose, eol } = parts.value;
|
|
280
|
+
const match = INLINE_TOOLS_RE.exec(fmBody);
|
|
281
|
+
if (!match)
|
|
282
|
+
return Err('no-tools-line');
|
|
283
|
+
const rawItems = toolItems(match[1]);
|
|
284
|
+
const kept = rawItems.filter(item => !isWholeServerTool(unquoteTool(item)));
|
|
285
|
+
const stamped = [...new Set(extraTools)].map(tool => `"${tool}"`);
|
|
286
|
+
// Nothing to remove and nothing to add: the line, whatever its spacing, is already right.
|
|
287
|
+
if (kept.length === rawItems.length && stamped.length === 0)
|
|
288
|
+
return Ok({ content, changed: false });
|
|
289
|
+
// The kept entries keep their own text; only the edges lose the padding that no longer
|
|
290
|
+
// borders a neighbour, so the appended entries land after `, ` and the line stays one shape.
|
|
291
|
+
const keptText = kept
|
|
292
|
+
.map((item, i) => {
|
|
293
|
+
const lead = i === 0 ? item.trimStart() : item;
|
|
294
|
+
return i === kept.length - 1 ? lead.trimEnd() : lead;
|
|
295
|
+
})
|
|
296
|
+
.join(',');
|
|
297
|
+
const inner = [keptText, stamped.join(', ')].filter(text => text !== '').join(', ');
|
|
298
|
+
const line = `tools: [${inner}]`;
|
|
299
|
+
const newBody = fmBody.slice(0, match.index) + line + fmBody.slice(match.index + match[0].length);
|
|
300
|
+
const newContent = openDelim + newBody + eol + closeDelim + afterClose;
|
|
301
|
+
return Ok({ content: newContent, changed: newContent !== content });
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The whole-server entries of the first frontmatter block's inline `tools:` line, in
|
|
305
|
+
* order. Ok([]) when the line carries none; an error when there is no line to read.
|
|
306
|
+
* Pure function — no I/O.
|
|
307
|
+
*/
|
|
308
|
+
export function readStampedTools(content) {
|
|
309
|
+
const parts = parseFrontmatter(content);
|
|
310
|
+
if (!parts.ok)
|
|
311
|
+
return Err(parts.error);
|
|
312
|
+
const match = INLINE_TOOLS_RE.exec(parts.value.fmBody);
|
|
313
|
+
if (!match)
|
|
314
|
+
return Err('no-tools-line');
|
|
315
|
+
return Ok(toolItems(match[1]).map(unquoteTool).filter(isWholeServerTool));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The text a converge installs for an agent: the variant `source` wearing the whole-server
|
|
319
|
+
* entries the installed copy carries. The stamp is applied at install and by
|
|
320
|
+
* `devflow tracker --servers`, never by a converge, so a converge only CARRIES it. Without
|
|
321
|
+
* the carry, switching an agent between prompt variants would drop its stamp and compare
|
|
322
|
+
* unequal to the installed copy on every run.
|
|
323
|
+
*
|
|
324
|
+
* `source` is returned unchanged when there is nothing safe to carry: either side has no
|
|
325
|
+
* well-formed inline `tools:` line, or the installed copy carries no entries.
|
|
326
|
+
* Pure function — no I/O.
|
|
327
|
+
*/
|
|
328
|
+
export function carryStampedTools(source, installed) {
|
|
329
|
+
const carried = readStampedTools(installed);
|
|
330
|
+
if (!carried.ok || carried.value.length === 0)
|
|
331
|
+
return source;
|
|
332
|
+
const stamped = stampAgentTools(source, carried.value);
|
|
333
|
+
return stamped.ok ? stamped.value.content : source;
|
|
334
|
+
}
|
|
234
335
|
//# sourceMappingURL=agent-frontmatter.js.map
|
|
@@ -172,13 +172,15 @@ function collectMissingFragmentWarnings(fnName, activeFrameworks, fragments, omi
|
|
|
172
172
|
*
|
|
173
173
|
* Every framework reference is installed on every machine
|
|
174
174
|
* (D-COMPLIANCE-INSTALL-ALWAYS), so file presence no longer says which
|
|
175
|
-
* frameworks apply. The
|
|
176
|
-
*
|
|
177
|
-
*
|
|
175
|
+
* frameworks apply. The command names the ids in the agent's prompt
|
|
176
|
+
* (D-COMPLIANCE-INJECT: the compliance directive) — the `COMPLIANCE` field of the
|
|
177
|
+
* settings line, which folds this machine's selection with the repository's
|
|
178
|
+
* `.devflow/project.json` — and those ids alone are in force. No agent takes a
|
|
179
|
+
* named lens input, so the stamp names the prompt, not a variable.
|
|
178
180
|
*/
|
|
179
181
|
const RUNTIME_SELECTION_LINES = [
|
|
180
182
|
'Every `references/{id}.md` is installed, so presence decides nothing: the ids you were given',
|
|
181
|
-
"(
|
|
183
|
+
"(named in your prompt; this machine's plus the repository's) are the frameworks in force.",
|
|
182
184
|
'Load `references/{id}.md` for each given id and no other; `none` means generic controls only.',
|
|
183
185
|
'Apply generic controls always.',
|
|
184
186
|
'',
|
|
@@ -106,6 +106,27 @@ async function readRawManifest(devflowDir) {
|
|
|
106
106
|
export async function readMachineFeature(devflowDir, feature) {
|
|
107
107
|
return isMachineFeatureOn(await readRawManifest(devflowDir), feature);
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a RAW parsed manifest has an opt-in `feature` switched on. Pure.
|
|
111
|
+
*
|
|
112
|
+
* D-BEHAVIOR-OPT-IN: only an explicit boolean `true` is on. A missing key, a
|
|
113
|
+
* non-boolean value, a manifest without a `features` record and anything that is not an
|
|
114
|
+
* object all read as OFF — fail-closed, the mirror of {@link isMachineFeatureOn}. Like
|
|
115
|
+
* that reader it is deliberately not built on readManifest(): a read-only status must
|
|
116
|
+
* not heal-write, and a manifest missing a required field is still a manifest here.
|
|
117
|
+
*/
|
|
118
|
+
export function isOptInFeatureOn(rawManifest, feature) {
|
|
119
|
+
if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
|
|
120
|
+
return false;
|
|
121
|
+
return rawManifest.features[feature] === true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read an opt-in machine switch from `<devflowDir>/manifest.json`. Read-only (no heal
|
|
125
|
+
* write); an absent, unreadable or malformed manifest reads as OFF.
|
|
126
|
+
*/
|
|
127
|
+
export async function readOptInFeature(devflowDir, feature) {
|
|
128
|
+
return isOptInFeatureOn(await readRawManifest(devflowDir), feature);
|
|
129
|
+
}
|
|
109
130
|
/**
|
|
110
131
|
* Write the machine-wide switch to `<devflowDir>/manifest.json` (atomic
|
|
111
132
|
* temp + rename). Refuses with `not-installed` when there is no manifest to
|
|
@@ -115,8 +136,8 @@ export async function readMachineFeature(devflowDir, feature) {
|
|
|
115
136
|
* D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
|
|
116
137
|
* nothing. The only bytes such a write could change are `updatedAt`, and that
|
|
117
138
|
* field means "the manifest's content last changed", so a no-op must not move
|
|
118
|
-
* it. This is the one write point of `devflow memory|learning|knowledge
|
|
119
|
-
* --enable/--disable`, so the rule holds for all
|
|
139
|
+
* it. This is the one write point of `devflow memory|learning|knowledge|behavior
|
|
140
|
+
* --enable/--disable`, so the rule holds for all four at once.
|
|
120
141
|
*/
|
|
121
142
|
export async function writeMachineFeature(devflowDir, feature, enabled) {
|
|
122
143
|
const raw = await readRawManifest(devflowDir);
|
package/dist/core/flags.js
CHANGED
|
@@ -88,6 +88,45 @@ export const FLAG_REGISTRY = [
|
|
|
88
88
|
recommended: true,
|
|
89
89
|
defaultValue: true,
|
|
90
90
|
},
|
|
91
|
+
{
|
|
92
|
+
// D-SUBAGENT-CACHE-5M: subagents write their prompt cache with the 5-minute TTL
|
|
93
|
+
// while the main session keeps the 1-hour TTL of `prompt-caching-1h` (untouched).
|
|
94
|
+
//
|
|
95
|
+
// Why: the wave-4 token audit (2026-10-10) found 99.998% of subagent cache writes
|
|
96
|
+
// are 1-hour writes, yet fewer than 0.1% of subagent requests idle longer than
|
|
97
|
+
// 5 minutes - a subagent finishes or is resumed long before a 5-minute entry
|
|
98
|
+
// lapses, so the 1-hour premium buys nothing. Modelled effect of 5m: -8.9% on
|
|
99
|
+
// subagent cache-equivalent cost, -7.7% on total.
|
|
100
|
+
//
|
|
101
|
+
// Mechanism (VENDOR BEHAVIOUR, probed 2026-10-11 on Claude Code 2.1.296 in a
|
|
102
|
+
// sandboxed session): with ENABLE_PROMPT_CACHING_1H=true and this setting at
|
|
103
|
+
// "5m", subagent requests wrote only ephemeral_5m cache (36,387 tokens, none at
|
|
104
|
+
// 1h) while the main session wrote 100% 1h (35,209). Precedence of the subagent
|
|
105
|
+
// TTL in the binary, highest first: an env var that forces 5m, env
|
|
106
|
+
// CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, this setting, a per-agent frontmatter
|
|
107
|
+
// `experimental.cacheTtl`, ENABLE_PROMPT_CACHING_1H. The 1-hour TTL is ignored
|
|
108
|
+
// while a subscription is in overage. A Claude Code that predates the setting
|
|
109
|
+
// ignores the key, so subagents then follow ENABLE_PROMPT_CACHING_1H as before.
|
|
110
|
+
// Re-probe when the precedence matters again; this is an observation, not a spec.
|
|
111
|
+
//
|
|
112
|
+
// Rejected: per-agent `experimental.cacheTtl` - it would spread one policy over
|
|
113
|
+
// every agent file; one devflow-managed flag keeps the policy in one place.
|
|
114
|
+
//
|
|
115
|
+
// Delete guard: only "5m" is deleted on disable or uninstall, so a user's own
|
|
116
|
+
// "1h" stays (D-GUARD-SCALAR). Adoption: init seeds this flag OFF when a value
|
|
117
|
+
// that is not "5m" is already on disk (D-ADOPT-RESPECTS-USER, respectUserOwnedSettings).
|
|
118
|
+
id: 'subagent-cache-5m',
|
|
119
|
+
label: 'Subagent 5-min cache',
|
|
120
|
+
description: 'Write subagent prompt-cache entries with the 5-minute TTL; the main session keeps 1 hour',
|
|
121
|
+
hint: 'Subagents rarely idle past 5 min, so 5-min cache writes cost less',
|
|
122
|
+
blurb: '5-min cache TTL for subagents',
|
|
123
|
+
kind: 'boolean',
|
|
124
|
+
target: { type: 'setting', key: 'subagentPromptCacheTtl' },
|
|
125
|
+
onPayload: '5m',
|
|
126
|
+
settingDeleteGuard: '5m',
|
|
127
|
+
recommended: true,
|
|
128
|
+
defaultValue: true,
|
|
129
|
+
},
|
|
91
130
|
{
|
|
92
131
|
id: 'show-turn-duration',
|
|
93
132
|
label: 'Show turn duration',
|
|
@@ -969,6 +1008,78 @@ export function settingHoldsManagedShape(settingsJson, flagId) {
|
|
|
969
1008
|
return false;
|
|
970
1009
|
}
|
|
971
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* D-ADOPT-RESPECTS-USER predicate: the mirror of `settingHoldsManagedShape`.
|
|
1013
|
+
*
|
|
1014
|
+
* Returns true when the settings.json string carries a value at the guarded flag's
|
|
1015
|
+
* target key that is NOT the flag's managed shape - a value the user (or another
|
|
1016
|
+
* tool) owns, which enabling the flag would overwrite. Presence is decided with
|
|
1017
|
+
* `hasOwnProperty`, so an explicit `null` counts as a value; the shape decision is
|
|
1018
|
+
* delegated to `settingValueHoldsManagedShape`, never compared here.
|
|
1019
|
+
*
|
|
1020
|
+
* Returns false when:
|
|
1021
|
+
* - settingsJson is malformed
|
|
1022
|
+
* - root is not a plain object
|
|
1023
|
+
* - flagId is not in the registry
|
|
1024
|
+
* - the flag has no settingDeleteGuard, or is not a setting-target boolean
|
|
1025
|
+
* - the key is absent
|
|
1026
|
+
* - the on-disk value holds the managed shape
|
|
1027
|
+
*/
|
|
1028
|
+
export function settingHoldsUserValue(settingsJson, flagId) {
|
|
1029
|
+
try {
|
|
1030
|
+
const parsed = JSON.parse(settingsJson);
|
|
1031
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
1032
|
+
return false;
|
|
1033
|
+
const flag = FLAG_REGISTRY_MAP.get(flagId);
|
|
1034
|
+
if (!flag || flag.kind !== 'boolean' || flag.target.type !== 'setting')
|
|
1035
|
+
return false;
|
|
1036
|
+
if (flag.settingDeleteGuard === undefined)
|
|
1037
|
+
return false;
|
|
1038
|
+
const root = parsed;
|
|
1039
|
+
if (!Object.prototype.hasOwnProperty.call(root, flag.target.key))
|
|
1040
|
+
return false;
|
|
1041
|
+
return !settingValueHoldsManagedShape(flag, root[flag.target.key]);
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* D-ADOPT-RESPECTS-USER: seed a guarded setting flag OFF when adopting it ON would
|
|
1049
|
+
* overwrite a value the user already owns.
|
|
1050
|
+
*
|
|
1051
|
+
* A flag the manifest does not record is adopted at its `defaultValue`. For a flag whose
|
|
1052
|
+
* target is a settings.json key with a `settingDeleteGuard`, adopting ON writes its
|
|
1053
|
+
* managed payload over whatever is on disk (the guard protects deletion, not writes).
|
|
1054
|
+
* When the key already holds a value that is not the managed shape - a user's own `"1h"`
|
|
1055
|
+
* under `subagentPromptCacheTtl` - that write would silently replace a deliberate choice.
|
|
1056
|
+
* Such a flag is set `false` instead; with the record then claiming `false`, the converge
|
|
1057
|
+
* pipeline neither writes nor deletes the user's value.
|
|
1058
|
+
*
|
|
1059
|
+
* Scope: only flags ABSENT from `manifestFlags` (or all of them on a fresh install, when
|
|
1060
|
+
* it is null). A flag the manifest records keeps its value in `flags`, so an explicit
|
|
1061
|
+
* enable still wins. `devflow init --reset` hands this an empty settings snapshot
|
|
1062
|
+
* (resolveResetGatedInputs), so nothing is detected and the factory default stands. The rule is generic over guarded
|
|
1063
|
+
* setting flags: no flag id is special-cased, and `settingHoldsUserValue` is the one place
|
|
1064
|
+
* that decides "the disk holds something that is not ours". For a flag whose default is
|
|
1065
|
+
* already OFF (suppress-attribution) it changes nothing.
|
|
1066
|
+
*
|
|
1067
|
+
* Two callers apply the same rule: the init seed, and the flags TUI's starting rows (an
|
|
1068
|
+
* upgrade that has not run init yet would otherwise offer a Save that overwrites the value).
|
|
1069
|
+
*
|
|
1070
|
+
* Pure function - the settings snapshot is read by the caller and passed in.
|
|
1071
|
+
*/
|
|
1072
|
+
export function respectUserOwnedSettings(flags, manifestFlags, settingsSnapshot) {
|
|
1073
|
+
const result = { ...flags };
|
|
1074
|
+
for (const flag of FLAG_REGISTRY) {
|
|
1075
|
+
const adoptedAtDefault = manifestFlags === null || !(flag.id in manifestFlags);
|
|
1076
|
+
if (!adoptedAtDefault)
|
|
1077
|
+
continue;
|
|
1078
|
+
if (settingHoldsUserValue(settingsSnapshot, flag.id))
|
|
1079
|
+
result[flag.id] = false;
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
972
1083
|
/**
|
|
973
1084
|
* D-ATTR-GUARD: returns true when it is safe to delete a flag's target key from
|
|
974
1085
|
* settings. For flags with a settingDeleteGuard the key is only deleted when the
|
package/dist/core/manifest.js
CHANGED
|
@@ -62,12 +62,15 @@ function parseManifestFlags(features, knownFlags) {
|
|
|
62
62
|
* - features.knownFlags stripped from result (folded into FlagsRecord key-presence)
|
|
63
63
|
* - features.proxy absent → false
|
|
64
64
|
* - features.compliance absent/malformed → {enabled:false, frameworks:[]}
|
|
65
|
+
* - features.behavior absent → false; malformed → false, healed on disk (D-BEHAVIOR-OPT-IN)
|
|
65
66
|
* - features.tracker absent/malformed → {provider:'github'}
|
|
67
|
+
* - features.tracker.servers absent → the default list; invalid → the default list, reported
|
|
68
|
+
* through `opts.onWarning` when the caller supplies one (this reader has no output channel)
|
|
66
69
|
*
|
|
67
70
|
* D39: heal-write failure returns the migrated in-memory manifest (not null).
|
|
68
71
|
* The on-disk format remains unhealed; next read triggers another attempt.
|
|
69
72
|
*/
|
|
70
|
-
export async function readManifest(devflowDir) {
|
|
73
|
+
export async function readManifest(devflowDir, opts) {
|
|
71
74
|
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
72
75
|
try {
|
|
73
76
|
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
@@ -129,7 +132,10 @@ export async function readManifest(devflowDir) {
|
|
|
129
132
|
features.decisions !== undefined ||
|
|
130
133
|
flagsWereLegacy ||
|
|
131
134
|
features.knownFlags !== undefined ||
|
|
132
|
-
features.viewMode !== undefined
|
|
135
|
+
features.viewMode !== undefined ||
|
|
136
|
+
// D-BEHAVIOR-OPT-IN: a malformed switch (present, not a boolean) is rewritten as
|
|
137
|
+
// the false it reads as; an absent one is not a defect and owes no write.
|
|
138
|
+
(features.behavior !== undefined && typeof features.behavior !== 'boolean');
|
|
133
139
|
const SECURITY_MODES = ['none', 'user', 'managed'];
|
|
134
140
|
const manifest = {
|
|
135
141
|
version: data.version,
|
|
@@ -158,12 +164,14 @@ export async function readManifest(devflowDir) {
|
|
|
158
164
|
proxy: typeof features.proxy === 'boolean' ? features.proxy : false,
|
|
159
165
|
// Self-heal: absent/malformed compliance → {enabled:false, frameworks:[]}
|
|
160
166
|
compliance: normalizeComplianceFeature(features.compliance),
|
|
167
|
+
// D-BEHAVIOR-OPT-IN: absent/malformed → false (opt-in; the inverse of knowledge/learning).
|
|
168
|
+
behavior: features.behavior === true,
|
|
161
169
|
// Self-heal: absent/malformed/unknown tracker → {provider:'github'} (AC-3.21).
|
|
162
170
|
// Deliberately NOT in the hard-null set above — see the field's doc comment.
|
|
163
171
|
// Healing here is silent and emits no DEGRADED: that is the correct
|
|
164
172
|
// self-heal behaviour, and a different condition from a per-repo config
|
|
165
173
|
// value outside the domain (which does emit `unknown tracker provider`).
|
|
166
|
-
tracker: normalizeTrackerFeature(features.tracker),
|
|
174
|
+
tracker: normalizeTrackerFeature(features.tracker, opts?.onWarning),
|
|
167
175
|
},
|
|
168
176
|
installedAt: data.installedAt,
|
|
169
177
|
updatedAt: data.updatedAt,
|