devflow-kit 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -1
- package/README.md +6 -2
- package/dist/agents/code.md +7 -11
- package/dist/agents/fix.md +5 -6
- package/dist/agents/refactor.md +5 -6
- package/dist/agents/review.md +2 -0
- package/dist/cli/commands/gates.js +276 -0
- package/dist/cli/commands/init-seed.js +9 -1
- package/dist/cli/commands/init.js +31 -8
- package/dist/cli/commands/memory.js +18 -0
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +2 -2
- package/dist/commands/code-review.md +52 -44
- package/dist/commands/debug.md +2 -2
- package/dist/commands/dynamic-build.md +53 -61
- package/dist/commands/dynamic-plan.md +2 -2
- package/dist/commands/dynamic-profile.md +2 -2
- package/dist/commands/dynamic-tickets.md +2 -2
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +26 -31
- package/dist/commands/plan.md +15 -25
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +2 -2
- package/dist/commands/resolve.md +2 -2
- package/dist/commands/self-review.md +2 -2
- package/dist/core/echo-value.js +42 -0
- package/dist/core/evidence-policy.js +17 -5
- package/dist/core/gate-level.js +213 -0
- package/dist/core/manifest.js +26 -10
- package/dist/core/model-discovery.js +7 -1
- package/dist/core/tracker.js +5 -33
- package/dist/learning-off/agents/code.md +7 -11
- package/dist/learning-off/agents/fix.md +5 -6
- package/dist/learning-off/agents/refactor.md +5 -6
- package/dist/learning-off/agents/review.md +2 -0
- package/dist/learning-off/commands/code-review.md +52 -44
- package/dist/learning-off/commands/debug.md +2 -2
- package/dist/learning-off/commands/dynamic-build.md +53 -61
- package/dist/learning-off/commands/explore.md +2 -2
- package/dist/learning-off/commands/implement.md +26 -31
- package/dist/learning-off/commands/plan.md +15 -25
- package/dist/learning-off/commands/resolve.md +2 -2
- package/dist/learning-off/commands/self-review.md +2 -2
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +7 -9
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +7 -15
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +7 -15
- package/dist/targets/claude-code/learning-install.js +15 -3
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +5 -6
- package/src/assets/agents/code.mds +2 -5
- package/src/assets/agents/curate.md +4 -6
- package/src/assets/agents/review.mds +2 -0
- package/src/assets/agents/synthesize.md +2 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_wave.mds +10 -17
- package/src/assets/commands/code-review.mds +50 -42
- package/src/assets/commands/dynamic-build.mds +41 -42
- package/src/assets/commands/implement.mds +24 -29
- package/src/assets/commands/plan.mds +14 -24
- package/src/assets/mds/git/_ops_steps.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +23 -12
- package/src/assets/mds/tracker/_github.mds +2 -9
- package/src/assets/mds/tracker/_jira.mds +2 -15
- package/src/assets/mds/tracker/_linear.mds +2 -15
- package/src/assets/scripts/ci-wait.cjs +22 -4
- package/src/assets/scripts/claude-md-audit.cjs +66 -51
- package/src/assets/scripts/hooks/background-memory-update +404 -95
- package/src/assets/scripts/hooks/json-helper.cjs +11 -0
- package/src/assets/scripts/hooks/json-parse +18 -3
- package/src/assets/scripts/hooks/memory-permission +358 -0
- package/src/assets/scripts/hooks/session-start-context +2 -2
- package/src/assets/scripts/hooks/session-start-memory +26 -5
- package/src/assets/scripts/lib/project-config.cjs +19 -4
- package/src/assets/scripts/resolve-settings.cjs +200 -32
- package/src/assets/skills/agent-references/SKILL.md +6 -2
- package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +2 -2
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
- package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
- package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
package/dist/commands/plan.md
CHANGED
|
@@ -5,8 +5,6 @@ description: Unified design planning - combines requirements discovery, gap anal
|
|
|
5
5
|
|
|
6
6
|
Orchestrate design planning from requirements discovery through gap analysis to implementation design. Produces a machine-readable design artifact consumed by `/implement`.
|
|
7
7
|
|
|
8
|
-
The orchestrator only spawns agents and gates — all analytical work is done by agents.
|
|
9
|
-
|
|
10
8
|
## Usage
|
|
11
9
|
|
|
12
10
|
```
|
|
@@ -38,8 +36,6 @@ Note: a bare digit run is a reference **only** under `github`, and that adjudica
|
|
|
38
36
|
|
|
39
37
|
## Clarification Gates
|
|
40
38
|
|
|
41
|
-
**MANDATORY**: Three gates that must complete before proceeding.
|
|
42
|
-
|
|
43
39
|
| Gate | Phase | Purpose |
|
|
44
40
|
|------|-------|---------|
|
|
45
41
|
| Gate 0 | Phase 1 | Requirements discovery before exploration |
|
|
@@ -109,7 +105,7 @@ If the user says "skip" or "just proceed" — skip remaining questions, present
|
|
|
109
105
|
|
|
110
106
|
#### Phase 2: Orient + Load Decisions
|
|
111
107
|
|
|
112
|
-
**Produces:** SKIM_CONTEXT, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, FEATURE_KNOWLEDGE_RULES
|
|
108
|
+
**Produces:** SKIM_CONTEXT, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, FEATURE_KNOWLEDGE_RULES, GATE_LEVEL
|
|
113
109
|
**Requires:** CONFIRMED_SCOPE
|
|
114
110
|
|
|
115
111
|
**Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
|
|
@@ -118,9 +114,9 @@ If the user says "skip" or "just proceed" — skip remaining questions, present
|
|
|
118
114
|
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
119
115
|
```
|
|
120
116
|
|
|
121
|
-
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
117
|
+
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off GATE_LEVEL=full` instead.
|
|
122
118
|
|
|
123
|
-
The accepted line is the only source of these values
|
|
119
|
+
The accepted line is the only source of these values.
|
|
124
120
|
|
|
125
121
|
Spawn Skim agent for codebase context:
|
|
126
122
|
|
|
@@ -238,10 +234,16 @@ A KB with no Rules section labels its entries `Rules ({section name}):` and give
|
|
|
238
234
|
|
|
239
235
|
Pass `FEATURE_KNOWLEDGE` to Explore and Design agents. Pass `DECISIONS_CONTEXT` to Design agents only.
|
|
240
236
|
|
|
237
|
+
**Gate level.** At `standard` or `lean`, once per run, run `d="${CLAUDE_CONFIG_DIR:-}"; case "$d" in /*) ;; *) d="$HOME/.claude" ;; esac; printf '%s\n' "$d"` in Bash and Read `<that directory>/skills/devflow:agent-references/references/commands/plan-levels.md` with the Read tool, and apply it to Phases 3-14.
|
|
238
|
+
|
|
239
|
+
- No row or rule overrides the next two lines.
|
|
240
|
+
- If the Read fails, or a row is missing or malformed, set `GATE_LEVEL` to `full` and say `level reference unreadable: full applied`.
|
|
241
|
+
- Security runs as its own Design spawn, and the design review runs, at every level and on every Revise pass.
|
|
242
|
+
|
|
241
243
|
#### Phase 3: Explore Requirements (Parallel)
|
|
242
244
|
|
|
243
245
|
**Produces:** EXPLORE_OUTPUTS
|
|
244
|
-
**Requires:** SKIM_CONTEXT
|
|
246
|
+
**Requires:** SKIM_CONTEXT, GATE_LEVEL
|
|
245
247
|
|
|
246
248
|
Spawn 4 `Agent(subagent_type="Explore")` agents **in a single message**, each with Skim agent context and `FEATURE_KNOWLEDGE: {feature_knowledge}` (from Phase 2). Include the instruction: "The FEATURE_KNOWLEDGE is a baseline — VALIDATE, EXTEND, and CORRECT it. For anything it already covers, cite its KB IDs (`{slug} KB-AP-n`) instead of restating the text. Focus on areas the feature knowledge doesn't cover and changes since it was last updated." Ask each agent for a final report of at most about 1,500 tokens: findings with file:line references, not file dumps.
|
|
247
249
|
|
|
@@ -257,8 +259,6 @@ Spawn 4 `Agent(subagent_type="Explore")` agents **in a single message**, each wi
|
|
|
257
259
|
**Produces:** EXPLORATION_SYNTHESIS
|
|
258
260
|
**Requires:** EXPLORE_OUTPUTS
|
|
259
261
|
|
|
260
|
-
**WAIT** for Phase 3 to complete.
|
|
261
|
-
|
|
262
262
|
```
|
|
263
263
|
Agent(subagent_type="Synthesize"):
|
|
264
264
|
"Synthesize EXPLORATION outputs for: {feature/issues}
|
|
@@ -319,7 +319,7 @@ Focus: {completeness|architecture|security|performance|compliance|consistency|de
|
|
|
319
319
|
DECISIONS_CONTEXT: {decisions_context}
|
|
320
320
|
FEATURE_KNOWLEDGE: {feature_knowledge}
|
|
321
321
|
{COMPLIANCE_FOCUS_DIRECTIVE} (compliance focus only)
|
|
322
|
-
|
|
322
|
+
Run `d="${CLAUDE_CONFIG_DIR:-}"; case "$d" in /*) ;; *) d="$HOME/.claude" ;; esac; printf '%s\n' "$d"` in Bash, then Read `<that directory>/skills/devflow:gap-analysis/references/compliance.md` with the Read tool and apply it as your focus patterns. (compliance focus only)
|
|
323
323
|
Artifacts:
|
|
324
324
|
Feature/Issues: {feature description or issue bodies}
|
|
325
325
|
Exploration synthesis: {Phase 4 output}
|
|
@@ -333,8 +333,6 @@ Cite evidence from provided artifacts."
|
|
|
333
333
|
**Produces:** GAP_SYNTHESIS
|
|
334
334
|
**Requires:** GAP_OUTPUTS
|
|
335
335
|
|
|
336
|
-
**WAIT** for Phase 5 to complete.
|
|
337
|
-
|
|
338
336
|
```
|
|
339
337
|
Agent(subagent_type="Synthesize"):
|
|
340
338
|
"Synthesize GAP ANALYSIS outputs for: {feature/issues}
|
|
@@ -395,8 +393,6 @@ Spawn 4 `Agent(subagent_type="Explore")` agents **in a single message**, each wi
|
|
|
395
393
|
**Produces:** IMPL_EXPLORATION_SYNTHESIS
|
|
396
394
|
**Requires:** IMPL_EXPLORE_OUTPUTS
|
|
397
395
|
|
|
398
|
-
**WAIT** for Phase 8 to complete.
|
|
399
|
-
|
|
400
396
|
```
|
|
401
397
|
Agent(subagent_type="Synthesize"):
|
|
402
398
|
"Synthesize IMPLEMENTATION EXPLORATION outputs for: {feature/issues}
|
|
@@ -410,7 +406,7 @@ Combine into: patterns to follow, integration points, reusable code, edge cases"
|
|
|
410
406
|
**Produces:** PLAN_OUTPUTS
|
|
411
407
|
**Requires:** IMPL_EXPLORATION_SYNTHESIS, GAP_SYNTHESIS
|
|
412
408
|
|
|
413
|
-
Spawn 3 Plan agents **in a single message**, each with implementation exploration synthesis. Ask each agent for a final report of at most about 1,500 tokens: the plan itself, not a restatement of the exploration.
|
|
409
|
+
Spawn 3 `Agent(subagent_type="Plan")` agents **in a single message**, each with implementation exploration synthesis. Ask each agent for a final report of at most about 1,500 tokens: the plan itself, not a restatement of the exploration.
|
|
414
410
|
|
|
415
411
|
| Focus | Output |
|
|
416
412
|
|-------|--------|
|
|
@@ -425,8 +421,6 @@ Implementation steps planner: include explicit gap mitigations (from Phase 6) in
|
|
|
425
421
|
**Produces:** PLANNING_SYNTHESIS
|
|
426
422
|
**Requires:** PLAN_OUTPUTS
|
|
427
423
|
|
|
428
|
-
**WAIT** for Phase 10 to complete.
|
|
429
|
-
|
|
430
424
|
```
|
|
431
425
|
Agent(subagent_type="Synthesize"):
|
|
432
426
|
"Synthesize PLANNING outputs for: {feature/issues}
|
|
@@ -627,6 +621,7 @@ Display completion summary:
|
|
|
627
621
|
- Issue URL (if created or enriched) — and any `TRACEABILITY: DEGRADED ({reason})` lines from the Git agent
|
|
628
622
|
- Gap analysis summary (N blocking, M should-address)
|
|
629
623
|
- Design review summary (N anti-patterns found, M mitigated in plan)
|
|
624
|
+
- Gate level: {GATE_LEVEL}, with any level note
|
|
630
625
|
- Suggested next step: `/implement {artifact-path}` or `/implement #{issue-number}`
|
|
631
626
|
|
|
632
627
|
---
|
|
@@ -670,13 +665,8 @@ Display completion summary:
|
|
|
670
665
|
|
|
671
666
|
## Principles
|
|
672
667
|
|
|
673
|
-
1. **
|
|
674
|
-
2. **
|
|
675
|
-
3. **Parallel execution** — Explore phases and gap analysis run in parallel; synthesis phases wait
|
|
676
|
-
4. **Evidence-based gaps** — Every gap cites specific text; no speculation
|
|
677
|
-
5. **Scope ruthlessly** — Small, focused plans ship faster; gate 1 enforces scope discipline
|
|
678
|
-
6. **Strict delegation** — Never synthesize, analyze, or plan in main session; always spawn agents
|
|
679
|
-
7. **Design artifacts are machine-readable** — `/implement` can consume the YAML frontmatter directly
|
|
668
|
+
1. **Scope ruthlessly** — Small, focused plans ship faster; gate 1 enforces scope discipline
|
|
669
|
+
2. **Strict delegation** — Never synthesize, analyze, or plan in main session; always spawn agents
|
|
680
670
|
|
|
681
671
|
## Error Handling
|
|
682
672
|
|
package/dist/commands/release.md
CHANGED
|
@@ -58,9 +58,9 @@ Read `.release/RELEASE-FLOW.md`:
|
|
|
58
58
|
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
61
|
+
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off GATE_LEVEL=full` instead.
|
|
62
62
|
|
|
63
|
-
The accepted line is the only source of these values
|
|
63
|
+
The accepted line is the only source of these values.
|
|
64
64
|
|
|
65
65
|
When the settings line says `LEARNING=off`, set `DECISIONS_CONTEXT` to `(none)` and skip this step, locating no ledger and reading no index.
|
|
66
66
|
|
|
@@ -47,9 +47,9 @@ and using its one-line output. If the command fails (outside a git repository),
|
|
|
47
47
|
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
50
|
+
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off GATE_LEVEL=full` instead.
|
|
51
51
|
|
|
52
|
-
The accepted line is the only source of these values
|
|
52
|
+
The accepted line is the only source of these values.
|
|
53
53
|
|
|
54
54
|
### Load DECISIONS_CONTEXT
|
|
55
55
|
|
package/dist/commands/resolve.md
CHANGED
|
@@ -94,9 +94,9 @@ Set `TARGET_DIR` to the selected review or bug-analysis directory path, and `TAR
|
|
|
94
94
|
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
97
|
+
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off GATE_LEVEL=full` instead.
|
|
98
98
|
|
|
99
|
-
The accepted line is the only source of these values
|
|
99
|
+
The accepted line is the only source of these values.
|
|
100
100
|
|
|
101
101
|
### Load DECISIONS_CONTEXT
|
|
102
102
|
|
|
@@ -30,9 +30,9 @@ Detect changed files and build context:
|
|
|
30
30
|
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
33
|
+
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off GATE_LEVEL=full` instead.
|
|
34
34
|
|
|
35
|
-
The accepted line is the only source of these values
|
|
35
|
+
The accepted line is the only source of these values.
|
|
36
36
|
|
|
37
37
|
### Load DECISIONS_CONTEXT
|
|
38
38
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render an untrusted value for a terminal message.
|
|
3
|
+
*
|
|
4
|
+
* A neutral home for the sanitizer that every parser echoing a rejected value
|
|
5
|
+
* shares (the tracker provider and the gate level today), so no parser imports
|
|
6
|
+
* another feature's module just to reach it.
|
|
7
|
+
*/
|
|
8
|
+
/** Longest rejected value echoed back to the terminal, in CODE POINTS. */
|
|
9
|
+
const MAX_ECHOED_VALUE = 40;
|
|
10
|
+
/**
|
|
11
|
+
* Render an untrusted token for a terminal message.
|
|
12
|
+
*
|
|
13
|
+
* Rejected values are echoed so the user can see what they typed, so they are
|
|
14
|
+
* third-party input at a display sink: control characters (terminal escapes,
|
|
15
|
+
* BEL, newlines) are replaced and the value is truncated.
|
|
16
|
+
*
|
|
17
|
+
* Takes `unknown`, and a non-string renders as its TYPE: the never-throws
|
|
18
|
+
* contract of the callers has to hold for what reaches this sink, not
|
|
19
|
+
* only for what the signature says — a caller reads hand-editable
|
|
20
|
+
* files, and a caller-side guard is one edit from being gone. Naming the type
|
|
21
|
+
* also keeps the render total, where `String()` would hand control to a
|
|
22
|
+
* caller-supplied `toString` — itself both a throw path and an echo path this
|
|
23
|
+
* function exists to close.
|
|
24
|
+
*/
|
|
25
|
+
export function describeEchoedValue(raw) {
|
|
26
|
+
if (typeof raw !== 'string')
|
|
27
|
+
return `<${raw === null ? 'null' : typeof raw}>`;
|
|
28
|
+
// The class is written with ESCAPES, never literal control bytes. A raw NUL
|
|
29
|
+
// makes grep classify this whole file as binary — it prints "Binary file
|
|
30
|
+
// matches" and skips the lines — so every grep-based sweep over src/core/
|
|
31
|
+
// silently stops covering the file while still exiting 0. Pinned by
|
|
32
|
+
// tests/guards/no-control-bytes.test.ts.
|
|
33
|
+
// eslint-disable-next-line no-control-regex
|
|
34
|
+
const stripped = raw.replace(/[\x00-\x1f\x7f]/g, '?');
|
|
35
|
+
// Cut by CODE POINT. `slice` cuts UTF-16 code units, so a cut landing inside an
|
|
36
|
+
// astral character (emoji, CJK ext-B) emits the lone surrogate half of it.
|
|
37
|
+
const points = [...stripped];
|
|
38
|
+
return points.length > MAX_ECHOED_VALUE
|
|
39
|
+
? `${points.slice(0, MAX_ECHOED_VALUE).join('')}…`
|
|
40
|
+
: stripped;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=echo-value.js.map
|
|
@@ -331,15 +331,27 @@ export function repoComplianceStatusLines(loaded, opts) {
|
|
|
331
331
|
* tracked by git, or null (D-PERSONAL-UNTRACKED). The resolver ignores such a file
|
|
332
332
|
* and says so on stderr, but prompts run it with stderr discarded, so a status
|
|
333
333
|
* command is where the user sees why their personal settings have no effect.
|
|
334
|
+
*
|
|
335
|
+
* Takes the ONE resolved result, so a caller that already holds it adds no second
|
|
336
|
+
* resolve (each resolve forks git).
|
|
337
|
+
*/
|
|
338
|
+
export function trackedConfigWarning(settings) {
|
|
339
|
+
if (!settings.personalTracked)
|
|
340
|
+
return null;
|
|
341
|
+
const file = settingsSourceFile('personal');
|
|
342
|
+
return `Git tracks an entry at or under ${file}, so devflow ignores that file. It holds personal settings. ` +
|
|
343
|
+
'Find the entry in the output of: git ls-files. Untrack it with: git rm --cached <entry>. ' +
|
|
344
|
+
'Then check the file contents, or delete the file. ' +
|
|
345
|
+
'Untracking leaves the committed bytes on disk as your personal file.';
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* {@link trackedConfigWarning} for a caller that holds the loaded module and not a
|
|
349
|
+
* resolved result: it resolves once and returns null when the module is unusable.
|
|
334
350
|
*/
|
|
335
351
|
export function personalConfigTrackedWarning(loaded, opts) {
|
|
336
352
|
if (!loaded.ok)
|
|
337
353
|
return null;
|
|
338
|
-
|
|
339
|
-
return null;
|
|
340
|
-
const file = settingsSourceFile('personal');
|
|
341
|
-
return `${file} is tracked by git, so devflow ignores it — it holds personal settings. ` +
|
|
342
|
-
`Untrack it with: git rm --cached ${file}`;
|
|
354
|
+
return trackedConfigWarning(loaded.value.resolveSettings(opts));
|
|
343
355
|
}
|
|
344
356
|
/** The policies the hint maps, in the order it prints them. */
|
|
345
357
|
const HINT_POLICIES = ['standard', 'required'];
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { promises as fs, constants as fsConstants } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
4
|
+
import { hasRequiredManifestFields } from './manifest.js';
|
|
5
|
+
import { describeEchoedValue } from './echo-value.js';
|
|
6
|
+
/**
|
|
7
|
+
* The gate level: how much checking a command applies. One of three words, from
|
|
8
|
+
* most to least checking.
|
|
9
|
+
*
|
|
10
|
+
* `features.gateLevel` in `~/.devflow/manifest.json` is the MACHINE default, and
|
|
11
|
+
* `gateLevel` in a repository's personal `.devflow/config.json` overrides it.
|
|
12
|
+
* Neither is a feature switch: a switch can only be narrowed by a repository
|
|
13
|
+
* layer, but a level may be raised or lowered by the personal file. The settings
|
|
14
|
+
* resolver (`resolve-settings.cjs`) folds both into the `GATE_LEVEL=` field of its
|
|
15
|
+
* settings line; this module owns the closed set, the parser and the machine
|
|
16
|
+
* layer's raw reader and writer. No code here chooses a level from a task or a
|
|
17
|
+
* diff.
|
|
18
|
+
*
|
|
19
|
+
* A present value that is not one of the three words is MALFORMED. A reader reads
|
|
20
|
+
* it as `full` and leaves it on disk as written: no read and no unrelated write
|
|
21
|
+
* heals it to absent, which would lower the level without a word.
|
|
22
|
+
*/
|
|
23
|
+
export const GATE_LEVELS = Object.freeze(['full', 'standard', 'lean']);
|
|
24
|
+
/** The level when neither the personal file nor the manifest records one. */
|
|
25
|
+
export const DEFAULT_GATE_LEVEL = 'standard';
|
|
26
|
+
/**
|
|
27
|
+
* Largest manifest read, in bytes. Pinned by a parity test to the resolver
|
|
28
|
+
* library's MAX_MANIFEST_BYTES, so the CLI and a prompt never disagree about a
|
|
29
|
+
* manifest that is too large to read.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_MANIFEST_BYTES = 1048576;
|
|
32
|
+
const VALID_LEVELS_LIST = GATE_LEVELS.join(', ');
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function isGateLevel(value) {
|
|
37
|
+
return typeof value === 'string' && GATE_LEVELS.includes(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Strict boundary parser for a gate level typed on the command line.
|
|
41
|
+
*
|
|
42
|
+
* REJECT, NEVER REPAIR: membership is byte-exact. No trim, no case folding, no
|
|
43
|
+
* alias. `Lean` and `lean ` are errors, not `lean`. The message names every valid
|
|
44
|
+
* level and echoes the offending value only in the sanitized form of
|
|
45
|
+
* {@link describeEchoedValue}.
|
|
46
|
+
*/
|
|
47
|
+
export function parseGateLevel(value) {
|
|
48
|
+
if (isGateLevel(value))
|
|
49
|
+
return { ok: true, value };
|
|
50
|
+
if (value === undefined || value === '') {
|
|
51
|
+
return { ok: false, error: `Missing gate level. Valid levels: ${VALID_LEVELS_LIST}` };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: `Unknown gate level: "${describeEchoedValue(value)}". Valid levels: ${VALID_LEVELS_LIST}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The machine layer's level in a RAW parsed manifest. Pure and total.
|
|
60
|
+
*
|
|
61
|
+
* null no level is recorded: not an object, `features` not an object, or
|
|
62
|
+
* no own `gateLevel` key
|
|
63
|
+
* 'invalid' a `gateLevel` key is present and is not one of the three words
|
|
64
|
+
* a level the recorded level
|
|
65
|
+
*
|
|
66
|
+
* Deliberately NOT built on readManifest(): that reader rejects a manifest
|
|
67
|
+
* missing a required field and heal-writes, and a read-only status must do
|
|
68
|
+
* neither. The resolver script's `machineGateLevel` is the CommonJS twin; a
|
|
69
|
+
* table test holds the two to the same rows.
|
|
70
|
+
*/
|
|
71
|
+
export function machineGateLevel(raw) {
|
|
72
|
+
if (!isPlainObject(raw))
|
|
73
|
+
return null;
|
|
74
|
+
const features = raw.features;
|
|
75
|
+
if (!isPlainObject(features))
|
|
76
|
+
return null;
|
|
77
|
+
if (!Object.hasOwn(features, 'gateLevel'))
|
|
78
|
+
return null;
|
|
79
|
+
const value = features.gateLevel;
|
|
80
|
+
return isGateLevel(value) ? value : 'invalid';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Set `features.gateLevel` in a RAW parsed manifest. Pure — returns a new object
|
|
84
|
+
* and never mutates its input. Null when the top level or `features` is not a
|
|
85
|
+
* plain object (there is no `features` record to write into).
|
|
86
|
+
*
|
|
87
|
+
* Only `features.gateLevel` and `updatedAt` change; every other key is carried
|
|
88
|
+
* verbatim, including keys this build does not model.
|
|
89
|
+
*/
|
|
90
|
+
export function setMachineGateLevel(raw, level, now) {
|
|
91
|
+
if (!isPlainObject(raw) || !isPlainObject(raw.features))
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
...raw,
|
|
95
|
+
features: { ...raw.features, gateLevel: level },
|
|
96
|
+
updatedAt: now,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const ABSENT = Object.freeze({ kind: 'absent' });
|
|
100
|
+
const CORRUPT = Object.freeze({ kind: 'corrupt' });
|
|
101
|
+
function startsWithBom(bytes) {
|
|
102
|
+
return bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
|
|
103
|
+
}
|
|
104
|
+
/** Bytes of the manifest, or why not: one bounded read of a regular file, followed through links. */
|
|
105
|
+
async function readManifestBytes(manifestPath) {
|
|
106
|
+
try {
|
|
107
|
+
let st;
|
|
108
|
+
try {
|
|
109
|
+
st = await fs.stat(manifestPath);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const code = err.code;
|
|
113
|
+
return code === 'ENOENT' || code === 'ENOTDIR' ? 'absent' : 'corrupt';
|
|
114
|
+
}
|
|
115
|
+
if (!st.isFile() || st.size > MAX_MANIFEST_BYTES)
|
|
116
|
+
return 'corrupt';
|
|
117
|
+
// O_NONBLOCK: a FIFO swapped in after the stat cannot block the open.
|
|
118
|
+
const handle = await fs.open(manifestPath, fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK || 0));
|
|
119
|
+
try {
|
|
120
|
+
const opened = await handle.stat();
|
|
121
|
+
if (!opened.isFile() || opened.size > MAX_MANIFEST_BYTES)
|
|
122
|
+
return 'corrupt';
|
|
123
|
+
// One byte past the stat'd size: a file that grew between the stat and the
|
|
124
|
+
// read is refused rather than truncated into something that parses. Each
|
|
125
|
+
// pass reads at least one byte or stops, so the loop is bounded by the buffer.
|
|
126
|
+
const buf = Buffer.alloc(opened.size + 1);
|
|
127
|
+
let total = 0;
|
|
128
|
+
for (let pass = 0; pass < buf.length && total < buf.length; pass++) {
|
|
129
|
+
const { bytesRead } = await handle.read(buf, total, buf.length - total, null);
|
|
130
|
+
if (bytesRead === 0)
|
|
131
|
+
break;
|
|
132
|
+
total += bytesRead;
|
|
133
|
+
}
|
|
134
|
+
return total > opened.size ? 'corrupt' : buf.subarray(0, total);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await handle.close().catch(() => undefined);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return 'corrupt';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Read `<devflowDir>/manifest.json` raw, for the gate level. Never throws, never
|
|
146
|
+
* exits and never writes: unlike readManifest() it heals nothing, models no key
|
|
147
|
+
* and keeps every key it does not name.
|
|
148
|
+
*
|
|
149
|
+
* It follows the resolver library's rules, so the CLI and a prompt see the same
|
|
150
|
+
* file: the path is followed through links, a missing path is absent, and every
|
|
151
|
+
* other failure is corrupt. Bytes decode as UTF-8 with a fatal error, which also
|
|
152
|
+
* strips a leading BOM, and then parse as JSON, so the level is read past a BOM
|
|
153
|
+
* as it is in the resolver. A file with a BOM is still `rejected`, because
|
|
154
|
+
* readManifest() parses the text as read and refuses it.
|
|
155
|
+
*/
|
|
156
|
+
export async function readGateManifest(devflowDir) {
|
|
157
|
+
const bytes = await readManifestBytes(path.join(devflowDir, 'manifest.json'));
|
|
158
|
+
if (bytes === 'absent')
|
|
159
|
+
return ABSENT;
|
|
160
|
+
if (bytes === 'corrupt')
|
|
161
|
+
return CORRUPT;
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return CORRUPT;
|
|
168
|
+
}
|
|
169
|
+
if (!isPlainObject(parsed) || !isPlainObject(parsed.features))
|
|
170
|
+
return CORRUPT;
|
|
171
|
+
const readable = !startsWithBom(bytes) && hasRequiredManifestFields(parsed);
|
|
172
|
+
return { kind: readable ? 'object' : 'rejected', raw: parsed };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Write the machine default to `<devflowDir>/manifest.json` (atomic temp +
|
|
176
|
+
* rename). Never throws. Refuses with `invalid-level`, before any I/O, a level
|
|
177
|
+
* that is not one of the three words, because the type is no check at run time.
|
|
178
|
+
* Refuses with `not-installed` when there is no manifest to write into — a
|
|
179
|
+
* manifest is created by `devflow init`, never by this write.
|
|
180
|
+
*
|
|
181
|
+
* Only `features.gateLevel` and `updatedAt` change. A write to the level the
|
|
182
|
+
* manifest already records changes nothing, `updatedAt` included, because that
|
|
183
|
+
* field means "the manifest's content last changed". A manifest that
|
|
184
|
+
* readManifest() would reject for another key gets the same write as one it
|
|
185
|
+
* accepts: its level is valid, and the fault is not this write's to fix.
|
|
186
|
+
*
|
|
187
|
+
* `now` is injected so a test fixes the timestamp.
|
|
188
|
+
*/
|
|
189
|
+
export async function writeMachineGateLevel(devflowDir, level, now) {
|
|
190
|
+
if (!isGateLevel(level))
|
|
191
|
+
return { kind: 'invalid-level' };
|
|
192
|
+
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
193
|
+
const read = await readGateManifest(devflowDir);
|
|
194
|
+
if (read.kind === 'absent')
|
|
195
|
+
return { kind: 'not-installed' };
|
|
196
|
+
if (read.kind === 'corrupt')
|
|
197
|
+
return { kind: 'unreadable', path: manifestPath };
|
|
198
|
+
const features = read.raw.features;
|
|
199
|
+
if (isPlainObject(features) && features.gateLevel === level)
|
|
200
|
+
return { kind: 'unchanged' };
|
|
201
|
+
try {
|
|
202
|
+
const next = setMachineGateLevel(read.raw, level, now());
|
|
203
|
+
if (next === null)
|
|
204
|
+
return { kind: 'unreadable', path: manifestPath };
|
|
205
|
+
await writeFileAtomicExclusive(manifestPath, JSON.stringify(next, null, 2) + '\n');
|
|
206
|
+
return { kind: 'written' };
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
const code = err?.code;
|
|
210
|
+
return { kind: 'write-failed', path: manifestPath, reason: typeof code === 'string' ? code : 'unknown error' };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=gate-level.js.map
|
package/dist/core/manifest.js
CHANGED
|
@@ -51,6 +51,27 @@ function parseManifestFlags(features, knownFlags) {
|
|
|
51
51
|
// Case C: missing/malformed → empty record
|
|
52
52
|
return { flags: {}, legacy: false };
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a parsed manifest object carries every field {@link readManifest}
|
|
56
|
+
* requires: a version, a plugin list, a `features` object with boolean `ambient`
|
|
57
|
+
* and `memory`, and string `installedAt` and `updatedAt`. Pure. A manifest that
|
|
58
|
+
* fails it reads as "no prior install".
|
|
59
|
+
*
|
|
60
|
+
* Exported so a raw reader can tell a manifest this reader rejects from one it
|
|
61
|
+
* accepts without calling it: readManifest heal-writes, and a read-only status
|
|
62
|
+
* must not.
|
|
63
|
+
*/
|
|
64
|
+
export function hasRequiredManifestFields(data) {
|
|
65
|
+
const features = data.features;
|
|
66
|
+
return !(!data.version ||
|
|
67
|
+
!Array.isArray(data.plugins) ||
|
|
68
|
+
typeof features !== 'object' ||
|
|
69
|
+
features === null ||
|
|
70
|
+
typeof features.ambient !== 'boolean' ||
|
|
71
|
+
typeof features.memory !== 'boolean' ||
|
|
72
|
+
typeof data.installedAt !== 'string' ||
|
|
73
|
+
typeof data.updatedAt !== 'string');
|
|
74
|
+
}
|
|
54
75
|
/**
|
|
55
76
|
* Read and parse the manifest file. Returns null if missing or corrupt.
|
|
56
77
|
*
|
|
@@ -75,17 +96,9 @@ export async function readManifest(devflowDir, opts) {
|
|
|
75
96
|
try {
|
|
76
97
|
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
77
98
|
const data = JSON.parse(content);
|
|
78
|
-
|
|
79
|
-
if (!data.version ||
|
|
80
|
-
!Array.isArray(data.plugins) ||
|
|
81
|
-
typeof features !== 'object' ||
|
|
82
|
-
features === null ||
|
|
83
|
-
typeof features.ambient !== 'boolean' ||
|
|
84
|
-
typeof features.memory !== 'boolean' ||
|
|
85
|
-
typeof data.installedAt !== 'string' ||
|
|
86
|
-
typeof data.updatedAt !== 'string') {
|
|
99
|
+
if (!hasRequiredManifestFields(data))
|
|
87
100
|
return null;
|
|
88
|
-
|
|
101
|
+
const features = data.features;
|
|
89
102
|
// D-FEATURES-ABSENT-ON (a sub-decision of D-FEATURES-NARROW-ONLY,
|
|
90
103
|
// src/core/feature-switch.ts): knowledge and learning are machine-wide
|
|
91
104
|
// switches, and every runtime gate — queue_read_gates in the hooks,
|
|
@@ -172,6 +185,9 @@ export async function readManifest(devflowDir, opts) {
|
|
|
172
185
|
// self-heal behaviour, and a different condition from a per-repo config
|
|
173
186
|
// value outside the domain (which does emit `unknown tracker provider`).
|
|
174
187
|
tracker: normalizeTrackerFeature(features.tracker, opts?.onWarning),
|
|
188
|
+
// The machine gate level, raw (see the field's doc comment): kept when the
|
|
189
|
+
// key is present, a stored `null` included, and left out when it is absent.
|
|
190
|
+
...(Object.hasOwn(features, 'gateLevel') ? { gateLevel: features.gateLevel } : {}),
|
|
175
191
|
},
|
|
176
192
|
installedAt: data.installedAt,
|
|
177
193
|
updatedAt: data.updatedAt,
|
|
@@ -61,9 +61,15 @@ const STDOUT_CAP = 262_144;
|
|
|
61
61
|
* within this window receives SIGTERM followed by a SIGKILL escalation after
|
|
62
62
|
* SIGKILL_GRACE_MS. Zero retries — the stale cache is the retry.
|
|
63
63
|
*
|
|
64
|
+
* The window must outlast a cold start on a loaded machine: the first exec of
|
|
65
|
+
* node with the routing runtime took 1.0-5.4 s in measurement while the OS vetted
|
|
66
|
+
* the new process. A shorter window reports an installed runtime as unknown. The
|
|
67
|
+
* test harness bounds (tests/model-discovery.test.ts) use a wider margin than this
|
|
68
|
+
* measured range.
|
|
69
|
+
*
|
|
64
70
|
* Exported so tests can derive timing bounds without mirroring the literal.
|
|
65
71
|
*/
|
|
66
|
-
export const SPAWN_TIMEOUT_MS =
|
|
72
|
+
export const SPAWN_TIMEOUT_MS = 6_000;
|
|
67
73
|
/**
|
|
68
74
|
* Grace period (ms) between SIGTERM and SIGKILL escalation. Unreffed so the
|
|
69
75
|
* timer never prevents the Node.js event loop from exiting on its own.
|
package/dist/core/tracker.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { promises as fs } from 'fs';
|
|
30
30
|
import * as path from 'path';
|
|
31
|
+
import { describeEchoedValue as describeTrackerValue } from './echo-value.js';
|
|
31
32
|
/**
|
|
32
33
|
* Canonical issue-tracker provider registry.
|
|
33
34
|
*
|
|
@@ -193,8 +194,6 @@ export const TRACKER_ATTEMPTS_MAX = 5;
|
|
|
193
194
|
// ---------------------------------------------------------------------------
|
|
194
195
|
const REGISTRY_SET = new Set(TRACKER_PROVIDER_IDS);
|
|
195
196
|
const VALID_IDS_LIST = TRACKER_PROVIDER_IDS.join(', ');
|
|
196
|
-
/** Longest rejected value echoed back to the terminal, in CODE POINTS. */
|
|
197
|
-
const MAX_ECHOED_VALUE = 40;
|
|
198
197
|
function errorMessage(err) {
|
|
199
198
|
return err instanceof Error ? err.message : String(err);
|
|
200
199
|
}
|
|
@@ -217,38 +216,11 @@ export function isTrackerProvider(value) {
|
|
|
217
216
|
return typeof value === 'string' && REGISTRY_SET.has(value);
|
|
218
217
|
}
|
|
219
218
|
/**
|
|
220
|
-
* Render an untrusted provider token for a terminal message.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* third-party input at a display sink: control characters (terminal escapes,
|
|
224
|
-
* BEL, newlines) are replaced and the value is truncated. Used by
|
|
225
|
-
* `parseTrackerId`'s error text and by `devflow tracker --status`.
|
|
226
|
-
*
|
|
227
|
-
* Takes `unknown`, and a non-string renders as its TYPE: the module's
|
|
228
|
-
* never-throws contract has to hold for what reaches this sink, not
|
|
229
|
-
* only for what the signature says does — `devflow tracker --status` reads
|
|
230
|
-
* a conventions file's hand-editable frontmatter, and a caller-side guard is one edit
|
|
231
|
-
* from being gone. Naming the type also keeps the render total, where `String()`
|
|
232
|
-
* would hand control to a caller-supplied `toString` — itself both a throw path
|
|
233
|
-
* and an echo path this function exists to close.
|
|
219
|
+
* Render an untrusted provider token for a terminal message. Used by
|
|
220
|
+
* `parseTrackerId`'s error text and by `devflow tracker --status`. The sanitizer
|
|
221
|
+
* itself lives in `echo-value.ts`; this is its tracker-facing name.
|
|
234
222
|
*/
|
|
235
|
-
export
|
|
236
|
-
if (typeof raw !== 'string')
|
|
237
|
-
return `<${raw === null ? 'null' : typeof raw}>`;
|
|
238
|
-
// The class is written with ESCAPES, never literal control bytes. A raw NUL
|
|
239
|
-
// makes grep classify this whole file as binary — it prints "Binary file
|
|
240
|
-
// matches" and skips the lines — so every grep-based sweep over src/core/
|
|
241
|
-
// silently stops covering tracker.ts while still exiting 0. Pinned by
|
|
242
|
-
// tests/guards/no-control-bytes.test.ts.
|
|
243
|
-
// eslint-disable-next-line no-control-regex
|
|
244
|
-
const stripped = raw.replace(/[\x00-\x1f\x7f]/g, '?');
|
|
245
|
-
// Cut by CODE POINT. `slice` cuts UTF-16 code units, so a cut landing inside an
|
|
246
|
-
// astral character (emoji, CJK ext-B) emits the lone surrogate half of it.
|
|
247
|
-
const points = [...stripped];
|
|
248
|
-
return points.length > MAX_ECHOED_VALUE
|
|
249
|
-
? `${points.slice(0, MAX_ECHOED_VALUE).join('')}…`
|
|
250
|
-
: stripped;
|
|
251
|
-
}
|
|
223
|
+
export { describeTrackerValue };
|
|
252
224
|
/**
|
|
253
225
|
* Strict boundary parser for a provider token (`--tracker <id>`,
|
|
254
226
|
* `devflow tracker --set <id>`).
|