@momentiq/dark-factory-cli 2.17.0 → 3.0.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/dist/adapters/codex-sdk.d.ts +30 -5
- package/dist/adapters/codex-sdk.d.ts.map +1 -1
- package/dist/adapters/codex-sdk.js +50 -15
- package/dist/adapters/codex-sdk.js.map +1 -1
- package/dist/adapters/cursor-sdk.d.ts.map +1 -1
- package/dist/adapters/cursor-sdk.js +14 -2
- package/dist/adapters/cursor-sdk.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/mode-scope.d.ts +86 -0
- package/dist/adapters/mode-scope.d.ts.map +1 -0
- package/dist/adapters/mode-scope.js +456 -0
- package/dist/adapters/mode-scope.js.map +1 -0
- package/dist/branch-protection/audit_branch_protection.py +80 -2
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +31 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/mode.d.ts +2 -0
- package/dist/commands/mode.d.ts.map +1 -0
- package/dist/commands/mode.js +235 -0
- package/dist/commands/mode.js.map +1 -0
- package/dist/commands/objectives.d.ts +27 -3
- package/dist/commands/objectives.d.ts.map +1 -1
- package/dist/commands/objectives.js +347 -53
- package/dist/commands/objectives.js.map +1 -1
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +3 -1
- package/dist/commands/onboard.js.map +1 -1
- package/dist/cycle-doc-validator/validate_cycle_doc.py +128 -11
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +15 -7
- package/dist/doctor.js.map +1 -1
- package/dist/evidence/index.d.ts +1 -0
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js +5 -0
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/ui-visual/embed.d.ts +65 -0
- package/dist/evidence/ui-visual/embed.d.ts.map +1 -0
- package/dist/evidence/ui-visual/embed.js +176 -0
- package/dist/evidence/ui-visual/embed.js.map +1 -0
- package/dist/evidence/ui-visual/index.d.ts +2 -0
- package/dist/evidence/ui-visual/index.d.ts.map +1 -0
- package/dist/evidence/ui-visual/index.js +6 -0
- package/dist/evidence/ui-visual/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/cycle-doc/parser.d.ts +3 -3
- package/dist/mcp/cycle-doc/parser.d.ts.map +1 -1
- package/dist/mcp/cycle-doc/parser.js +2 -2
- package/dist/mcp/cycle-doc/parser.js.map +1 -1
- package/dist/mcp/tools/doctor.d.ts +5 -0
- package/dist/mcp/tools/doctor.d.ts.map +1 -1
- package/dist/mcp/tools/doctor.js +10 -1
- package/dist/mcp/tools/doctor.js.map +1 -1
- package/dist/mcp/tools/review-bypass.d.ts +5 -0
- package/dist/mcp/tools/review-bypass.d.ts.map +1 -1
- package/dist/mcp/tools/review-bypass.js +9 -1
- package/dist/mcp/tools/review-bypass.js.map +1 -1
- package/dist/mode/apply.d.ts +8 -0
- package/dist/mode/apply.d.ts.map +1 -0
- package/dist/mode/apply.js +118 -0
- package/dist/mode/apply.js.map +1 -0
- package/dist/mode/context.d.ts +50 -0
- package/dist/mode/context.d.ts.map +1 -0
- package/dist/mode/context.js +127 -0
- package/dist/mode/context.js.map +1 -0
- package/dist/mode/critic-registration.d.ts +66 -0
- package/dist/mode/critic-registration.d.ts.map +1 -0
- package/dist/mode/critic-registration.js +279 -0
- package/dist/mode/critic-registration.js.map +1 -0
- package/dist/mode/guard.d.ts +15 -0
- package/dist/mode/guard.d.ts.map +1 -0
- package/dist/mode/guard.js +160 -0
- package/dist/mode/guard.js.map +1 -0
- package/dist/mode/init.d.ts +17 -0
- package/dist/mode/init.d.ts.map +1 -0
- package/dist/mode/init.js +147 -0
- package/dist/mode/init.js.map +1 -0
- package/dist/mode/loadConfig.d.ts +34 -0
- package/dist/mode/loadConfig.d.ts.map +1 -0
- package/dist/mode/loadConfig.js +38 -0
- package/dist/mode/loadConfig.js.map +1 -0
- package/dist/mode/prompts/designer-baseline.md +47 -0
- package/dist/mode/resolve.d.ts +24 -0
- package/dist/mode/resolve.d.ts.map +1 -0
- package/dist/mode/resolve.js +65 -0
- package/dist/mode/resolve.js.map +1 -0
- package/dist/mode/scope-policy.d.ts +41 -0
- package/dist/mode/scope-policy.d.ts.map +1 -0
- package/dist/mode/scope-policy.js +97 -0
- package/dist/mode/scope-policy.js.map +1 -0
- package/dist/mode/status.d.ts +14 -0
- package/dist/mode/status.d.ts.map +1 -0
- package/dist/mode/status.js +13 -0
- package/dist/mode/status.js.map +1 -0
- package/dist/onboard/seeders/design-md.d.ts +3 -0
- package/dist/onboard/seeders/design-md.d.ts.map +1 -0
- package/dist/onboard/seeders/design-md.js +138 -0
- package/dist/onboard/seeders/design-md.js.map +1 -0
- package/dist/onboard/seeders/index.d.ts +8 -0
- package/dist/onboard/seeders/index.d.ts.map +1 -1
- package/dist/onboard/seeders/index.js +3 -0
- package/dist/onboard/seeders/index.js.map +1 -1
- package/dist/onboard/validate.d.ts +36 -1
- package/dist/onboard/validate.d.ts.map +1 -1
- package/dist/onboard/validate.js +136 -42
- package/dist/onboard/validate.js.map +1 -1
- package/dist/skills/config.d.ts +2 -110
- package/dist/skills/config.d.ts.map +1 -1
- package/dist/skills/config.js +20 -58
- package/dist/skills/config.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/install.d.ts.map +1 -1
- package/dist/skills/install.js +7 -0
- package/dist/skills/install.js.map +1 -1
- package/package.json +3 -3
- package/skills/designer-brief/SKILL.md.tmpl +74 -0
- package/skills/designer-brief/skill.json +17 -0
- package/skills/designer-build/SKILL.md.tmpl +63 -0
- package/skills/designer-build/skill.json +17 -0
- package/skills/designer-handoff/SKILL.md.tmpl +64 -0
- package/skills/designer-handoff/skill.json +17 -0
- package/skills/designer-ship/SKILL.md.tmpl +77 -0
- package/skills/designer-ship/skill.json +17 -0
- package/skills/objectives/SKILL.md.tmpl +34 -6
- package/skills/verify/producer/ui-visual-surfaces.ts +261 -0
- package/skills/verify/producer/ui-visual.config.ts +22 -0
- package/skills/verify/producer/ui-visual.producer.spec.ts +137 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Cycle 27 Phase C — register the deterministic ModeScope critic into a
|
|
2
|
+
// consumer's `.agent-review/config.json`.
|
|
3
|
+
//
|
|
4
|
+
// `df mode init` calls this after seeding the `modes` catalog so the scope
|
|
5
|
+
// critic RUNS in local `df review` / `df gate-push` AND in CI `agent-critic`
|
|
6
|
+
// (inside the existing required check — no consumer ruleset/workflow
|
|
7
|
+
// changes). The mirror of static-schema-lint's posture: the adapter ships
|
|
8
|
+
// available-but-off in `ADAPTER_LOADERS`; a config reference is what turns
|
|
9
|
+
// it on for a repo.
|
|
10
|
+
//
|
|
11
|
+
// QUORUM SAFETY (the subtle part). The scope critic is a deterministic,
|
|
12
|
+
// veto-only critic that ALWAYS completes. Under `min-complete-quorum`, the
|
|
13
|
+
// gate needs `quorum` critics to COMPLETE. Naively appending an
|
|
14
|
+
// always-completing critic to the fleet WITHOUT touching quorum would let
|
|
15
|
+
// it substitute for an LLM completion — silently LOWERING the number of LLM
|
|
16
|
+
// chief-engineers that must complete. `df critic` aggregates on TWO paths
|
|
17
|
+
// and the scope critic (added to `critics[]`) runs on BOTH, so BOTH quorums
|
|
18
|
+
// are bumped by exactly 1:
|
|
19
|
+
// - the root `aggregation.quorum`, which governs the PROFILE-LESS run the
|
|
20
|
+
// reusable workflow's CONSUMER path uses (`df critic` with no
|
|
21
|
+
// `AGENT_REVIEW_PROFILE` → `resolveCriticProfile` → undefined → whole
|
|
22
|
+
// `critics[]` fleet under the root aggregation); AND
|
|
23
|
+
// - every `profile.quorum`, for EXPLICIT-profile runs like dark-factory's
|
|
24
|
+
// own `AGENT_REVIEW_PROFILE=cloud` CI (filters to `profile.criticIds`).
|
|
25
|
+
// The bumps are independent — a single run uses exactly one aggregation
|
|
26
|
+
// config (root OR one profile) — so they never double-count. The engineer
|
|
27
|
+
// reviewing the init PR sees each bump reported in `df mode init`'s output.
|
|
28
|
+
//
|
|
29
|
+
// Operates on the RAW parsed JSON (not the normalized AgentReviewConfig) so
|
|
30
|
+
// consumer customizations are preserved byte-for-byte except the fields we
|
|
31
|
+
// touch. The caller re-validates the mutated object through
|
|
32
|
+
// `parseAgentReviewConfig` (fail-closed) before writing.
|
|
33
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { parseAgentReviewConfig } from "@momentiq/dark-factory-schemas";
|
|
36
|
+
import { MODE_SCOPE_ADAPTER_ID } from "../adapters/mode-scope.js";
|
|
37
|
+
/** The critic id used for the ModeScope critic in `.agent-review/config.json`. */
|
|
38
|
+
export const MODE_SCOPE_CRITIC_ID = "mode-scope-critic";
|
|
39
|
+
function isRecord(v) {
|
|
40
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The critic entry appended to `critics[]`. Deterministic critic: no auth,
|
|
44
|
+
* no vendor SDK. `required: true` — the scope critic is a HARD GATE, so its
|
|
45
|
+
* blockers must block under EVERY aggregation policy. Under `block-if-any`
|
|
46
|
+
* the gate demotes `required: false` critics' findings to warnings (report.ts
|
|
47
|
+
* / gate.ts key on `config.critics.filter(c => c.required)`), so a
|
|
48
|
+
* `required: false` scope critic would NOT block an out-of-scope `Mode:` PR
|
|
49
|
+
* there. `required: true` is also safe under `min-complete-quorum`: that path
|
|
50
|
+
* (`quorumAggregateVerdict`) never consults the `required` flag, and because
|
|
51
|
+
* the adapter ALWAYS completes it never trips the "required critic errored →
|
|
52
|
+
* block" rule — so it only ever adds the veto its blocker already carries, and
|
|
53
|
+
* the per-path quorum bumps below stay exact. `runtime: "local"` is metadata —
|
|
54
|
+
* the same deterministic entry is referenced by both the local and cloud
|
|
55
|
+
* profiles (mirrors how `cursor-*` is shared across profiles).
|
|
56
|
+
*/
|
|
57
|
+
export function buildModeScopeCriticEntry() {
|
|
58
|
+
return {
|
|
59
|
+
id: MODE_SCOPE_CRITIC_ID,
|
|
60
|
+
name: "Mode Scope Critic — deterministic operator-mode scope enforcement",
|
|
61
|
+
adapter: MODE_SCOPE_ADAPTER_ID,
|
|
62
|
+
required: true,
|
|
63
|
+
runtime: "local",
|
|
64
|
+
model: { id: "deterministic", params: [] },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Bump the ROOT `aggregation.quorum` by 1 (min-complete-quorum only). Governs
|
|
69
|
+
* the PROFILE-LESS run: the reusable workflow's consumer path invokes
|
|
70
|
+
* `df critic` with no `AGENT_REVIEW_PROFILE`, so the whole `critics[]` fleet —
|
|
71
|
+
* now including the always-completing scope critic — is aggregated under the
|
|
72
|
+
* root policy, and its LLM completion floor must be preserved. Under
|
|
73
|
+
* block-if-any there is no quorum to preserve (a blocker vetoes directly), so
|
|
74
|
+
* nothing is bumped.
|
|
75
|
+
*
|
|
76
|
+
* Called EXACTLY ONCE, at the moment the scope critic is first ADDED to
|
|
77
|
+
* `critics[]` (a fresh registration) — never on a converge re-run. Quorum is a
|
|
78
|
+
* bare integer, so "already bumped to N" is indistinguishable from "should be
|
|
79
|
+
* N+1 but wasn't"; a converge that re-bumped would inflate the floor without
|
|
80
|
+
* bound. Presence of the entry in `critics[]` means the root fleet already
|
|
81
|
+
* accounts for it. (This is the exact-once transition key.)
|
|
82
|
+
*/
|
|
83
|
+
function bumpRootQuorum(raw, changes) {
|
|
84
|
+
const aggregation = raw["aggregation"];
|
|
85
|
+
if (isRecord(aggregation) &&
|
|
86
|
+
aggregation["policy"] === "min-complete-quorum" &&
|
|
87
|
+
typeof aggregation["quorum"] === "number") {
|
|
88
|
+
const prev = aggregation["quorum"];
|
|
89
|
+
aggregation["quorum"] = prev + 1;
|
|
90
|
+
changes.push(`aggregation.quorum bumped ${prev}→${prev + 1} (governs profile-less runs incl. the reusable workflow's consumer path; preserves the LLM completion floor)`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Ensure `criticId` is listed in EVERY profile's `criticIds`, bumping the
|
|
95
|
+
* quorum by 1 ONLY for each profile it was newly ADDED to. The add and the
|
|
96
|
+
* bump are coupled and keyed on the absent→present transition: a profile that
|
|
97
|
+
* already lists the critic is left untouched (no double-bump). This is what
|
|
98
|
+
* makes both fresh registration and the converge path idempotent — re-running
|
|
99
|
+
* `df mode init` never inflates a profile that already accounts for the scope
|
|
100
|
+
* critic, and it repairs a profile that a partial migration left it out of.
|
|
101
|
+
*/
|
|
102
|
+
function ensureCriticInEveryProfile(raw, criticId, changes) {
|
|
103
|
+
const profiles = raw["profiles"];
|
|
104
|
+
if (!isRecord(profiles))
|
|
105
|
+
return;
|
|
106
|
+
for (const [name, profileRaw] of Object.entries(profiles)) {
|
|
107
|
+
if (!isRecord(profileRaw))
|
|
108
|
+
continue;
|
|
109
|
+
const ids = Array.isArray(profileRaw["criticIds"])
|
|
110
|
+
? profileRaw["criticIds"]
|
|
111
|
+
: [];
|
|
112
|
+
if (ids.includes(criticId))
|
|
113
|
+
continue; // already accounted for — no re-bump.
|
|
114
|
+
ids.push(criticId);
|
|
115
|
+
profileRaw["criticIds"] = ids;
|
|
116
|
+
if (typeof profileRaw["quorum"] === "number") {
|
|
117
|
+
const prev = profileRaw["quorum"];
|
|
118
|
+
profileRaw["quorum"] = prev + 1;
|
|
119
|
+
changes.push(`profile "${name}": added scope critic + bumped quorum ${prev}→${prev + 1} (preserves the LLM completion floor)`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
changes.push(`profile "${name}": added scope critic`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Register the ModeScope critic into a raw parsed `.agent-review/config.json`
|
|
128
|
+
* object. Mutates `raw` in place AND returns a change report. The caller
|
|
129
|
+
* validates the mutated object through `parseAgentReviewConfig` before
|
|
130
|
+
* persisting it.
|
|
131
|
+
*
|
|
132
|
+
* "Present" is NOT "safe". A pre-existing mode-scope entry can itself be
|
|
133
|
+
* UNSAFE — an older CLI (or a hand edit) may have added it `required: false`
|
|
134
|
+
* (its blockers are demoted to warnings under block-if-any) or left it out of a
|
|
135
|
+
* profile (it does not run at all under profile-filtered runs). So rather than
|
|
136
|
+
* no-op the instant an entry is present, we CONVERGE it to the required shape:
|
|
137
|
+
* promote `required: true`, repair a missing id, and ensure the id is in every
|
|
138
|
+
* profile. Only a FULLY compliant entry is a true no-op (`changed: false`).
|
|
139
|
+
*
|
|
140
|
+
* Quorum is preserved EXACTLY ONCE per scope, keyed on the absent→present
|
|
141
|
+
* transition (`bumpRootQuorum` for the root fleet, `ensureCriticInEveryProfile`
|
|
142
|
+
* per profile), so a converge re-run never double-counts.
|
|
143
|
+
*/
|
|
144
|
+
export function registerModeScopeCritic(raw) {
|
|
145
|
+
if (!isRecord(raw)) {
|
|
146
|
+
throw new Error("`.agent-review/config.json` is not a JSON object");
|
|
147
|
+
}
|
|
148
|
+
const critics = Array.isArray(raw["critics"])
|
|
149
|
+
? raw["critics"]
|
|
150
|
+
: [];
|
|
151
|
+
raw["critics"] = critics;
|
|
152
|
+
const existing = critics.filter((c) => isRecord(c) && c["adapter"] === MODE_SCOPE_ADAPTER_ID);
|
|
153
|
+
if (existing.length > 1) {
|
|
154
|
+
// Ambiguous / corrupt: two always-completing scope critics double-count the
|
|
155
|
+
// quorum floor and there is no single canonical entry to converge. Fail
|
|
156
|
+
// CLOSED — the caller surfaces this as `error` and `df mode init` fails loud
|
|
157
|
+
// (never a silent "already-registered"). Judgment call: we throw rather than
|
|
158
|
+
// auto-dedup, because choosing which duplicate to keep and reconciling their
|
|
159
|
+
// differing required/id/profile state is not safely inferable.
|
|
160
|
+
throw new Error(`\`.agent-review/config.json\` has ${existing.length} critics with adapter ` +
|
|
161
|
+
`"${MODE_SCOPE_ADAPTER_ID}"; expected at most one. Remove the duplicate ` +
|
|
162
|
+
`entries and re-run \`df mode init\`.`);
|
|
163
|
+
}
|
|
164
|
+
const changes = [];
|
|
165
|
+
if (existing.length === 0) {
|
|
166
|
+
// FRESH registration: add the entry, then preserve the floor once on each
|
|
167
|
+
// scope it newly enters (the root fleet + every profile).
|
|
168
|
+
critics.push(buildModeScopeCriticEntry());
|
|
169
|
+
changes.push(`added critic "${MODE_SCOPE_CRITIC_ID}" (adapter: ${MODE_SCOPE_ADAPTER_ID}, required: true, deterministic hard gate)`);
|
|
170
|
+
bumpRootQuorum(raw, changes);
|
|
171
|
+
ensureCriticInEveryProfile(raw, MODE_SCOPE_CRITIC_ID, changes);
|
|
172
|
+
return { changed: true, changes };
|
|
173
|
+
}
|
|
174
|
+
// CONVERGE the single pre-existing entry to the required shape.
|
|
175
|
+
const entry = existing[0];
|
|
176
|
+
// 1. Canonical id. Profiles reference critics BY id, so keep an existing
|
|
177
|
+
// non-empty string id (forcing a new one would orphan those references)
|
|
178
|
+
// and only repair a missing / non-string one. Judgment call: we do NOT
|
|
179
|
+
// rename a differing existing id — it is the consumer's chosen id and
|
|
180
|
+
// renaming is not safely reversible; we converge profiles to whatever id
|
|
181
|
+
// the entry actually uses.
|
|
182
|
+
let criticId;
|
|
183
|
+
if (typeof entry["id"] === "string" && entry["id"].length > 0) {
|
|
184
|
+
criticId = entry["id"];
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
entry["id"] = MODE_SCOPE_CRITIC_ID;
|
|
188
|
+
criticId = MODE_SCOPE_CRITIC_ID;
|
|
189
|
+
changes.push(`set the missing id on the existing mode-scope critic to "${MODE_SCOPE_CRITIC_ID}"`);
|
|
190
|
+
}
|
|
191
|
+
// 2. Promote to required: true — the hard-gate invariant. A required: false
|
|
192
|
+
// scope critic's blockers are demoted to warnings under block-if-any.
|
|
193
|
+
if (entry["required"] !== true) {
|
|
194
|
+
const was = entry["required"];
|
|
195
|
+
entry["required"] = true;
|
|
196
|
+
changes.push(`promoted the existing mode-scope critic "${criticId}" to required: true ` +
|
|
197
|
+
`(hard-gate invariant; was ${JSON.stringify(was)})`);
|
|
198
|
+
}
|
|
199
|
+
// 3. Ensure the id is in every profile (adds + bumps only the profiles that
|
|
200
|
+
// were missing it). Already-listing profiles are left alone — their
|
|
201
|
+
// presence means they already account for the critic.
|
|
202
|
+
//
|
|
203
|
+
// WONTFIX (documented, #311): the ROOT `aggregation.quorum` is likewise
|
|
204
|
+
// NOT re-bumped on converge. A hand-corrupted config where `mode-scope` is
|
|
205
|
+
// in `critics[]` but the root quorum was never bumped is OUTSIDE this
|
|
206
|
+
// converge contract's repair scope, for two reasons: (1) the CLI never
|
|
207
|
+
// produces that state — the fresh path bumps root atomically with the add
|
|
208
|
+
// (the caller writes the fully-mutated object or nothing), and no released
|
|
209
|
+
// CLI registers `mode-scope` at all, so it is reachable only by hand-edit;
|
|
210
|
+
// (2) root-bump status is UNOBSERVABLE from a bare integer — `N` cannot be
|
|
211
|
+
// told apart from `(N+1) minus one` — so any in-registration "repair" is a
|
|
212
|
+
// blind guess that either over-inflates the already-correct case or runs
|
|
213
|
+
// away on re-runs (breaking idempotency). Detection of this hand-edit-only
|
|
214
|
+
// drift is deferred to a read-only `df doctor` check (#333); registration
|
|
215
|
+
// stays idempotent.
|
|
216
|
+
ensureCriticInEveryProfile(raw, criticId, changes);
|
|
217
|
+
return { changed: changes.length > 0, changes };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Register the ModeScope critic into `<repoRoot>/.agent-review/config.json`.
|
|
221
|
+
*
|
|
222
|
+
* - `absent`: no config.json — the consumer hasn't run `df onboard`. Skipped
|
|
223
|
+
* (not an error): the catalog seed still stands and re-running init after
|
|
224
|
+
* onboarding completes registration.
|
|
225
|
+
* - `already-registered`: the critic is already present — idempotent no-op.
|
|
226
|
+
* - `registered`: patched + written.
|
|
227
|
+
* - `error`: the config is malformed OR the patched config fails canonical
|
|
228
|
+
* validation. The on-disk file is left UNTOUCHED (fail-closed); the error
|
|
229
|
+
* is reported, not thrown, so `df mode init` degrades gracefully.
|
|
230
|
+
*/
|
|
231
|
+
export function applyModeScopeCriticRegistration(repoRoot) {
|
|
232
|
+
const configPath = join(repoRoot, ".agent-review", "config.json");
|
|
233
|
+
if (!existsSync(configPath)) {
|
|
234
|
+
return { configPath, status: "absent", changes: [] };
|
|
235
|
+
}
|
|
236
|
+
let raw;
|
|
237
|
+
try {
|
|
238
|
+
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
return {
|
|
242
|
+
configPath,
|
|
243
|
+
status: "error",
|
|
244
|
+
changes: [],
|
|
245
|
+
error: `not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
let result;
|
|
249
|
+
try {
|
|
250
|
+
result = registerModeScopeCritic(raw); // mutates `raw` in place
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
return {
|
|
254
|
+
configPath,
|
|
255
|
+
status: "error",
|
|
256
|
+
changes: [],
|
|
257
|
+
error: e instanceof Error ? e.message : String(e),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (!result.changed) {
|
|
261
|
+
return { configPath, status: "already-registered", changes: [] };
|
|
262
|
+
}
|
|
263
|
+
// Fail closed: the mutated object MUST parse canonically before we persist
|
|
264
|
+
// it. On failure the on-disk file is never rewritten.
|
|
265
|
+
try {
|
|
266
|
+
parseAgentReviewConfig(raw);
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
return {
|
|
270
|
+
configPath,
|
|
271
|
+
status: "error",
|
|
272
|
+
changes: [],
|
|
273
|
+
error: `patched config failed validation (not written): ${e instanceof Error ? e.message : String(e)}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`);
|
|
277
|
+
return { configPath, status: "registered", changes: result.changes };
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=critic-registration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"critic-registration.js","sourceRoot":"","sources":["../../src/mode/critic-registration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ModeConfig } from "@momentiq/dark-factory-schemas";
|
|
2
|
+
export { globMatch } from "./scope-policy.js";
|
|
3
|
+
export interface ResolvedModeLike {
|
|
4
|
+
modeName: string;
|
|
5
|
+
mode: ModeConfig;
|
|
6
|
+
}
|
|
7
|
+
export type GuardDecision = {
|
|
8
|
+
decision: "allow";
|
|
9
|
+
} | {
|
|
10
|
+
decision: "deny";
|
|
11
|
+
plain: string;
|
|
12
|
+
hint: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function decideGuard(input: unknown, resolved: ResolvedModeLike | undefined): GuardDecision;
|
|
15
|
+
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/mode/guard.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAQjE,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,GACrB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAuItD,wBAAgB,WAAW,CACzB,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GACrC,aAAa,CAwCf"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Cycle 27 Phase B — `df mode guard --hook` pure decision core.
|
|
2
|
+
//
|
|
3
|
+
// The Designer Mode tool guard, bound to Claude Code's PreToolUse hook by
|
|
4
|
+
// commands/mode.ts (stdin JSON → decideGuard → hook decision JSON). Layer 2
|
|
5
|
+
// of the enforcement stack (spec §4.4): soft relative to the pre-push gate
|
|
6
|
+
// and CI ModeScope critic (layers 3–4), but the layer that turns a wrong
|
|
7
|
+
// turn into instant, plain-language feedback inside the session.
|
|
8
|
+
//
|
|
9
|
+
// Posture:
|
|
10
|
+
// - No active mode → allow everything: the guard must be invisible to
|
|
11
|
+
// normal engineer sessions.
|
|
12
|
+
// - Active mode → file-writing tools are scope-checked (protectedPaths
|
|
13
|
+
// WINS over writeScopes on overlap); `dependencyPolicy: "none"` denies
|
|
14
|
+
// manifest/lockfile edits and dependency-adding Bash; destructive git
|
|
15
|
+
// is denied; read-only tools pass.
|
|
16
|
+
// - Active mode + input we cannot understand → fail CLOSED. A guard that
|
|
17
|
+
// fails open under confusion is not a guard.
|
|
18
|
+
//
|
|
19
|
+
// Denials are dual-register: `plain` speaks to a non-technical operator
|
|
20
|
+
// (no jargon, no file-path vocabulary beyond the path itself); `hint`
|
|
21
|
+
// speaks to the agent (which config knob fired, what to do instead —
|
|
22
|
+
// usually: offer a handoff, never retry the same write).
|
|
23
|
+
import { relative, resolve, sep } from "node:path";
|
|
24
|
+
import { classifyModePath } from "./scope-policy.js";
|
|
25
|
+
// `globMatch` historically lived in this file and is imported from here by
|
|
26
|
+
// tests + external callers. Its canonical home is now scope-policy.ts
|
|
27
|
+
// (shared with the CI scope critic so layers 2 and 4 enforce one policy —
|
|
28
|
+
// spec §4.4); re-export keeps the old import path working.
|
|
29
|
+
export { globMatch } from "./scope-policy.js";
|
|
30
|
+
const ALLOW = { decision: "allow" };
|
|
31
|
+
const FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
32
|
+
const DESTRUCTIVE_BASH = [
|
|
33
|
+
{ re: /\bgit\s+push\b[^\n;|&]*\s(--force\b|-f\b|--force-with-lease\b)/, what: "force-push" },
|
|
34
|
+
{ re: /\bgit\s+reset\s+--hard\b/, what: "hard reset" },
|
|
35
|
+
{ re: /\bgit\s+clean\s+-[a-zA-Z]*f/, what: "git clean" },
|
|
36
|
+
{ re: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f|\brm\s+-[a-zA-Z]*f[a-zA-Z]*r/, what: "recursive delete" },
|
|
37
|
+
];
|
|
38
|
+
const DEPENDENCY_BASH = /\b(npm\s+(install|i|add)\s+\S|yarn\s+add\b|pnpm\s+(add|install)\s+\S|bun\s+add\b)/;
|
|
39
|
+
// Bash is DEFAULT-DENY while a mode is active: shell writes (`> file`,
|
|
40
|
+
// `sed -i`, `cp`, arbitrary interpreters) would bypass writeScopes entirely,
|
|
41
|
+
// and no denylist can enumerate a shell. Only read-only inspection and the
|
|
42
|
+
// repo's own dev/test scripts are allowed — and only as single, unchained
|
|
43
|
+
// commands (metacharacters disqualify even safe-list prefixes).
|
|
44
|
+
const BASH_METACHARS = /[;&|<>`$\\\n\r]/;
|
|
45
|
+
const BASH_ALLOWLIST = [
|
|
46
|
+
/^git (status|log|diff|show|branch)(\s|$)/,
|
|
47
|
+
// NOTE: no `find` — its -delete/-exec/-ok forms mutate; the agent has
|
|
48
|
+
// dedicated read-only search tools instead.
|
|
49
|
+
/^(ls|pwd|cat|head|tail|grep|rg|echo|which)(\s|$)/,
|
|
50
|
+
/^npm (test|start)$/,
|
|
51
|
+
// `npm run <script>` (+ optional `--workspace=<name>`): RATIFIED trusted, same
|
|
52
|
+
// basis as `mode.devServer.command` below — package scripts are repo-author-
|
|
53
|
+
// committed. Contingent on package.json being operator-non-editable in these
|
|
54
|
+
// modes: `checkPath` denies manifest edits under `dependencyPolicy: "none"` and
|
|
55
|
+
// package.json sits in the seeded `protectedPaths`, so an operator cannot add a
|
|
56
|
+
// script — the reachable set is exactly what engineers vetted at commit time.
|
|
57
|
+
// No per-mode `allowedScripts` allowlist: it would duplicate those author-
|
|
58
|
+
// controls and desync from real scripts without narrowing an already author-
|
|
59
|
+
// bounded surface. The script-name charset admits no args and BASH_METACHARS is
|
|
60
|
+
// rejected first (checkBash), so there is no `-- <shell>` passthrough or
|
|
61
|
+
// chaining. Revisit only if a mode ever leaves package.json operator-editable.
|
|
62
|
+
/^npm run [A-Za-z0-9:_./-]+(\s--workspace=[A-Za-z0-9@/._-]+)?$/,
|
|
63
|
+
/^(node|npm|npx|git) --version$/,
|
|
64
|
+
];
|
|
65
|
+
function deny(plain, hint) {
|
|
66
|
+
return { decision: "deny", plain, hint };
|
|
67
|
+
}
|
|
68
|
+
function failClosed(modeName) {
|
|
69
|
+
return deny(`To be safe, ${modeName} mode blocked an action it couldn't understand.`, `guard received unparseable PreToolUse input while mode "${modeName}" is active — fail-closed; re-issue the tool call with well-formed input or hand off`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Repo-relative, forward-slash, `..`-free path — or null when it escapes the
|
|
73
|
+
* repo. BOTH absolute and relative inputs are canonicalized through
|
|
74
|
+
* `resolve(cwd, filePath)` first: a relative path with `..` segments
|
|
75
|
+
* ("src/components/../../server/db.ts") must be scope-checked against what
|
|
76
|
+
* it RESOLVES to, never against its raw text.
|
|
77
|
+
*/
|
|
78
|
+
function relativize(filePath, cwd) {
|
|
79
|
+
const rel = relative(cwd, resolve(cwd, filePath));
|
|
80
|
+
const normalized = rel.split(sep).join("/");
|
|
81
|
+
if (normalized === "" || normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
function checkPath(path, resolved) {
|
|
87
|
+
const { modeName, mode } = resolved;
|
|
88
|
+
// Precedence (protected > dependency > out-of-scope) lives in the shared
|
|
89
|
+
// classifier so this hook (layer 2) and the CI scope critic (layer 4)
|
|
90
|
+
// cannot drift — spec §4.4. This function maps the classification to the
|
|
91
|
+
// guard's dual-register deny messages.
|
|
92
|
+
const classification = classifyModePath(path, mode);
|
|
93
|
+
switch (classification.kind) {
|
|
94
|
+
case "protected":
|
|
95
|
+
return deny(`"${path}" is a protected area that needs an engineer — ${modeName} mode can't change it.`, `path matches mode "${modeName}" protectedPaths (protected wins over writeScopes) — do not retry; offer a handoff (df handoff) describing the intended change`);
|
|
96
|
+
case "dependency":
|
|
97
|
+
return deny(`Adding or changing installed packages isn't something ${modeName} mode can do.`, `dependencyPolicy is "none" for mode "${modeName}" — dependency changes escalate to an engineer via handoff`);
|
|
98
|
+
case "out-of-scope":
|
|
99
|
+
return deny(`"${path}" is outside the design areas set up for you, so ${modeName} mode left it unchanged.`, `path matches no writeScopes entry for mode "${modeName}" — if the task genuinely needs this file, stop and offer a handoff instead of retrying`);
|
|
100
|
+
case "allow":
|
|
101
|
+
return ALLOW;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function checkBash(command, resolved) {
|
|
105
|
+
const { modeName, mode } = resolved;
|
|
106
|
+
const trimmed = command.trim();
|
|
107
|
+
// Specific denials first — they carry sharper reasons than the default.
|
|
108
|
+
for (const { re, what } of DESTRUCTIVE_BASH) {
|
|
109
|
+
if (re.test(trimmed)) {
|
|
110
|
+
return deny(`That command could destroy work (${what}), so ${modeName} mode blocked it.`, `destructive command class "${what}" is always denied in operator modes — never needed for design work`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (mode.dependencyPolicy === "none" && DEPENDENCY_BASH.test(trimmed)) {
|
|
114
|
+
return deny(`Installing new packages isn't something ${modeName} mode can do.`, `dependencyPolicy is "none" for mode "${modeName}" — dependency additions escalate via handoff`);
|
|
115
|
+
}
|
|
116
|
+
// The catalog's own dev-server command is repo-author-committed — trusted
|
|
117
|
+
// verbatim (exact match only).
|
|
118
|
+
if (mode.devServer?.command && trimmed === mode.devServer.command.trim()) {
|
|
119
|
+
return ALLOW;
|
|
120
|
+
}
|
|
121
|
+
// Default-deny with a curated allowlist: single unchained commands only.
|
|
122
|
+
if (!BASH_METACHARS.test(trimmed) && BASH_ALLOWLIST.some((re) => re.test(trimmed))) {
|
|
123
|
+
return ALLOW;
|
|
124
|
+
}
|
|
125
|
+
return deny(`That command isn't on ${modeName} mode's safe list, so it wasn't run.`, `bash is default-deny in mode "${modeName}" (shell writes would bypass writeScopes) — use the file tools for edits inside writeScopes, the configured devServer command for previews, or offer a handoff`);
|
|
126
|
+
}
|
|
127
|
+
export function decideGuard(input, resolved) {
|
|
128
|
+
// Layer-2 invisibility: no active mode, no opinions.
|
|
129
|
+
if (!resolved)
|
|
130
|
+
return ALLOW;
|
|
131
|
+
if (typeof input !== "object" || input === null)
|
|
132
|
+
return failClosed(resolved.modeName);
|
|
133
|
+
const hook = input;
|
|
134
|
+
if (typeof hook.tool_name !== "string")
|
|
135
|
+
return failClosed(resolved.modeName);
|
|
136
|
+
const toolInput = typeof hook.tool_input === "object" && hook.tool_input !== null
|
|
137
|
+
? hook.tool_input
|
|
138
|
+
: {};
|
|
139
|
+
if (FILE_TOOLS.has(hook.tool_name)) {
|
|
140
|
+
const rawPath = toolInput["file_path"] ?? toolInput["notebook_path"];
|
|
141
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) {
|
|
142
|
+
return failClosed(resolved.modeName);
|
|
143
|
+
}
|
|
144
|
+
const cwd = typeof hook.cwd === "string" && hook.cwd.length > 0 ? hook.cwd : process.cwd();
|
|
145
|
+
const rel = relativize(rawPath, cwd);
|
|
146
|
+
if (rel === null) {
|
|
147
|
+
return deny(`That file is outside this project, so ${resolved.modeName} mode left it alone.`, `path resolves outside the repo root — always denied in operator modes`);
|
|
148
|
+
}
|
|
149
|
+
return checkPath(rel, resolved);
|
|
150
|
+
}
|
|
151
|
+
if (hook.tool_name === "Bash") {
|
|
152
|
+
const command = toolInput["command"];
|
|
153
|
+
if (typeof command !== "string")
|
|
154
|
+
return failClosed(resolved.modeName);
|
|
155
|
+
return checkBash(command, resolved);
|
|
156
|
+
}
|
|
157
|
+
// Read-only / non-mutating tools pass through untouched.
|
|
158
|
+
return ALLOW;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/mode/guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ModeConfig } from "@momentiq/dark-factory-schemas";
|
|
2
|
+
export interface InitModeResult {
|
|
3
|
+
status: "created" | "exists";
|
|
4
|
+
modeName: string;
|
|
5
|
+
configPath: string;
|
|
6
|
+
writeScopes: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Propose write scopes from directories that exist. Monorepo `apps/<name>`
|
|
10
|
+
* workspaces are scanned one level deep. Styling files are always in scope —
|
|
11
|
+
* a designer mode with no reachable surface still gets `**\/*.css` so the
|
|
12
|
+
* engineer sees an editable (if minimal) seed to correct in the init PR.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectWriteScopes(repoRoot: string): string[];
|
|
15
|
+
export declare function buildDefaultModeEntry(repoRoot: string, modeName: string): ModeConfig;
|
|
16
|
+
export declare function initMode(repoRoot: string, modeName: string): InitModeResult;
|
|
17
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/mode/init.ts"],"names":[],"mappings":"AAgBA,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,gCAAgC,CAAC;AAIxC,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAkCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAiB5D;AAkBD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAcpF;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,CAwD3E"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Cycle 27 Phase A — `df mode init <name>`: seed a `modes.<name>` catalog
|
|
2
|
+
// entry into the consumer's darkfactory.yaml.
|
|
3
|
+
//
|
|
4
|
+
// Design constraints:
|
|
5
|
+
// - APPEND, never re-serialize the consumer's file: hand-maintained comments
|
|
6
|
+
// and formatting survive. The appended block is generated YAML.
|
|
7
|
+
// - Deterministic scope proposal: only directories that actually exist are
|
|
8
|
+
// proposed (the engineer reviews the init PR — this is a seed, not policy).
|
|
9
|
+
// - Idempotent: an existing `modes.<name>` entry short-circuits to "exists".
|
|
10
|
+
// - Fail closed: the candidate file content must round-trip through the
|
|
11
|
+
// canonical parser (@momentiq/dark-factory-schemas) before it is written.
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
15
|
+
import { MODE_NAME_RE, parseDarkFactoryConfig, } from "@momentiq/dark-factory-schemas";
|
|
16
|
+
import { loadModeConfig } from "./loadConfig.js";
|
|
17
|
+
const DEFAULT_PROTECTED_PATHS = [
|
|
18
|
+
"**/auth/**",
|
|
19
|
+
"**/api/**",
|
|
20
|
+
"**/migrations/**",
|
|
21
|
+
".github/**",
|
|
22
|
+
"**/*.config.*",
|
|
23
|
+
"package.json",
|
|
24
|
+
"package-lock.json",
|
|
25
|
+
];
|
|
26
|
+
const ROOT_SCOPE_DIR_CANDIDATES = [
|
|
27
|
+
"src/components",
|
|
28
|
+
"src/app",
|
|
29
|
+
"src/pages",
|
|
30
|
+
"src/styles",
|
|
31
|
+
"app",
|
|
32
|
+
"components",
|
|
33
|
+
"pages",
|
|
34
|
+
"styles",
|
|
35
|
+
"public",
|
|
36
|
+
];
|
|
37
|
+
const APP_SCOPE_SUBDIR_CANDIDATES = ["src/components", "src/app", "src/pages", "public"];
|
|
38
|
+
function isDir(path) {
|
|
39
|
+
try {
|
|
40
|
+
return statSync(path).isDirectory();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Propose write scopes from directories that exist. Monorepo `apps/<name>`
|
|
48
|
+
* workspaces are scanned one level deep. Styling files are always in scope —
|
|
49
|
+
* a designer mode with no reachable surface still gets `**\/*.css` so the
|
|
50
|
+
* engineer sees an editable (if minimal) seed to correct in the init PR.
|
|
51
|
+
*/
|
|
52
|
+
export function detectWriteScopes(repoRoot) {
|
|
53
|
+
const scopes = [];
|
|
54
|
+
for (const candidate of ROOT_SCOPE_DIR_CANDIDATES) {
|
|
55
|
+
if (isDir(join(repoRoot, candidate)))
|
|
56
|
+
scopes.push(`${candidate}/**`);
|
|
57
|
+
}
|
|
58
|
+
const appsRoot = join(repoRoot, "apps");
|
|
59
|
+
if (isDir(appsRoot)) {
|
|
60
|
+
for (const entry of readdirSync(appsRoot).sort()) {
|
|
61
|
+
for (const sub of APP_SCOPE_SUBDIR_CANDIDATES) {
|
|
62
|
+
if (isDir(join(appsRoot, entry, sub))) {
|
|
63
|
+
scopes.push(`apps/${entry}/${sub}/**`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
scopes.push("**/*.css");
|
|
69
|
+
return scopes;
|
|
70
|
+
}
|
|
71
|
+
function detectDevServer(repoRoot) {
|
|
72
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
73
|
+
if (!existsSync(pkgPath))
|
|
74
|
+
return undefined;
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
77
|
+
if (pkg.scripts?.["dev"]) {
|
|
78
|
+
return { command: "npm run dev", url: "http://localhost:3000" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Malformed package.json → no dev-server proposal; init still succeeds.
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
export function buildDefaultModeEntry(repoRoot, modeName) {
|
|
87
|
+
const writeScopes = detectWriteScopes(repoRoot);
|
|
88
|
+
const devServer = detectDevServer(repoRoot);
|
|
89
|
+
const entry = {
|
|
90
|
+
writeScopes,
|
|
91
|
+
protectedPaths: [...DEFAULT_PROTECTED_PATHS],
|
|
92
|
+
dependencyPolicy: "none",
|
|
93
|
+
diffBudget: { maxFiles: 15, maxLines: 600 },
|
|
94
|
+
designSystem: { designMd: "DESIGN.md" },
|
|
95
|
+
evidence: { visual: "required" },
|
|
96
|
+
};
|
|
97
|
+
if (devServer)
|
|
98
|
+
entry.devServer = devServer;
|
|
99
|
+
void modeName; // reserved: future per-mode-name defaults (e.g. "pm")
|
|
100
|
+
return entry;
|
|
101
|
+
}
|
|
102
|
+
export function initMode(repoRoot, modeName) {
|
|
103
|
+
if (!MODE_NAME_RE.test(modeName)) {
|
|
104
|
+
// Validated BEFORE any file work: the name is interpolated into
|
|
105
|
+
// generated YAML (comment lines + map key) — newlines or comment
|
|
106
|
+
// breakouts are injection, not typos. Same rule the canonical parser
|
|
107
|
+
// enforces, applied early for a better error.
|
|
108
|
+
throw new Error(`df mode init: invalid mode name ${JSON.stringify(modeName)} — mode names must be ` +
|
|
109
|
+
`lowercase slugs matching ${MODE_NAME_RE.source} (e.g. "designer").`);
|
|
110
|
+
}
|
|
111
|
+
// Shared read → parseYaml → parseDarkFactoryConfig ritual (with the raw
|
|
112
|
+
// `text` init appends to) lives in loadModeConfig — one interpretation of
|
|
113
|
+
// darkfactory.yaml across the `df mode …` surface.
|
|
114
|
+
const { configPath, text: existingText, config: existingConfig } = loadModeConfig(repoRoot);
|
|
115
|
+
const entry = buildDefaultModeEntry(repoRoot, modeName);
|
|
116
|
+
if (existingConfig.modes?.[modeName]) {
|
|
117
|
+
return {
|
|
118
|
+
status: "exists",
|
|
119
|
+
modeName,
|
|
120
|
+
configPath,
|
|
121
|
+
writeScopes: existingConfig.modes[modeName]?.writeScopes ?? [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (existingConfig.modes) {
|
|
125
|
+
// Appending a second top-level `modes:` block would produce a duplicate
|
|
126
|
+
// YAML key. Rare (multi-mode repos) — fail with a precise manual step
|
|
127
|
+
// rather than re-serializing the consumer's file and destroying comments.
|
|
128
|
+
throw new Error(`df mode init: darkfactory.yaml already has a \`modes\` catalog ` +
|
|
129
|
+
`(without "${modeName}"). Add the entry under the existing \`modes:\` ` +
|
|
130
|
+
`key manually — df will not rewrite your file.`);
|
|
131
|
+
}
|
|
132
|
+
const block = `\n# Operator-mode catalog (cycle 27) — seeded by \`df mode init ${modeName}\`.\n` +
|
|
133
|
+
`# Review the proposed scopes: protectedPaths always wins over writeScopes.\n` +
|
|
134
|
+
stringifyYaml({ modes: { [modeName]: entry } });
|
|
135
|
+
const separator = existingText.length === 0 || existingText.endsWith("\n") ? "" : "\n";
|
|
136
|
+
const candidate = existingText + separator + block;
|
|
137
|
+
// Fail closed: the file we are about to write must parse canonically.
|
|
138
|
+
parseDarkFactoryConfig(parseYaml(candidate));
|
|
139
|
+
writeFileSync(configPath, candidate);
|
|
140
|
+
return {
|
|
141
|
+
status: "created",
|
|
142
|
+
modeName,
|
|
143
|
+
configPath,
|
|
144
|
+
writeScopes: entry.writeScopes ?? [],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=init.js.map
|