create-agentic-workspace 0.0.0 → 0.2.2

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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # create-agentic-workspace
2
+
3
+ The pre-session bootstrap wizard for an [Agentic Foundry](https://github.com/lukasrepublic/agentic-foundry)
4
+ workspace. `/foundry:init` can never scaffold its own permission floor — a model editing its own
5
+ confinement is a shape the platform's own classifier denies — so the floor is written **before a
6
+ session exists**, in the operator's own terminal.
7
+
8
+ ```bash
9
+ npx create-agentic-workspace --dir my-workspace
10
+ ```
11
+
12
+ The CLI walks you through the target directory, greenfield-vs-existing, git/GitHub identity, and
13
+ stage mode, **previews every file it will write and every capability it will declare**, writes
14
+ the workspace, and stops. It never runs `claude`, never accepts the workspace trust dialog, and
15
+ never pre-grants anything — it *declares*, the platform's trust dialog is the consent ceremony.
16
+
17
+ ## What it does
18
+
19
+ - Emits the plugin's reviewed three-tier permission map verbatim into the new workspace's
20
+ committed `.claude/settings.json`, alongside `extraKnownMarketplaces` and `enabledPlugins`
21
+ pinned to an exact marketplace ref (`autoUpdate: false` — no floating grant).
22
+ - Absorbs `foundry-bootstrap.sh`'s out-of-session `git` commit-identity isolation (`--gh-account`),
23
+ proved differentially equal to the shipped script.
24
+ - Scaffolds a seven-file, schema-valid workspace seed.
25
+ - Re-running is a **reconcile with a drift report** — an edited managed file is reported
26
+ `drifted` and left byte-identical, never overwritten. Never-clobber is unconditional.
27
+
28
+ ## Flags
29
+
30
+ Run `npx create-agentic-workspace --help` for the full, single-sourced flag list (every flag has
31
+ an interactive-prompt twin, and `--yes` never prompts).
32
+
33
+ ## No telemetry
34
+
35
+ This CLI collects and transmits nothing — **no telemetry** of any kind, and no opt-out to offer
36
+ because there is nothing to opt out of. No credential is read, derived, or written; the one
37
+ optional `gh api user` identity probe reads whatever authentication your own `gh` already holds
38
+ without ever persisting, logging, or printing it beyond the name/email you confirm.
39
+
40
+ ## Supply-chain posture
41
+
42
+ Zero third-party dependencies, `scripts` closed to `{test}` (no lifecycle hook of any kind), and
43
+ every `import` a `node:` built-in or a relative path. See the plugin's own
44
+ [Security posture](https://github.com/lukasrepublic/agentic-foundry/blob/main/specs/features/foundry/onboarding/bootstrap-cli/feat-foundry-bootstrap-cli.md)
45
+ for the honest limits of what a build-time check over the source tree can and cannot attest about
46
+ a published tarball.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ // create-agentic-workspace — the npx entrypoint (AC-BCL-2). Argv parsing itself lives in
3
+ // ../src/argv.mjs, derived from the one question table in ../src/questions.mjs; this file only
4
+ // wires process.argv/stdio to the orchestrator and prints its output.
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import os from 'node:os';
8
+ import { runCli } from '../src/run.mjs';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const pkgDir = path.resolve(__dirname, '..');
12
+
13
+ // A bare leading positional (`create-agentic-workspace my-app`) is sugar for `--dir my-app`
14
+ // (prior art: create-next-app/create-vite). It is translated here, before the table-derived
15
+ // argv parser ever sees it — the parser's own accepted-flag set stays exactly the question
16
+ // table (AC-BCL-2's bijection).
17
+ const rawArgv = process.argv.slice(2);
18
+ let argv = rawArgv;
19
+ if (rawArgv.length > 0 && !rawArgv[0].startsWith('--')) {
20
+ argv = ['--dir', rawArgv[0], ...rawArgv.slice(1)];
21
+ }
22
+
23
+ const { exitCode, output } = await runCli(argv, {
24
+ cwd: process.cwd(),
25
+ isTTY: Boolean(process.stdin.isTTY),
26
+ input: process.stdin,
27
+ output: process.stdout,
28
+ homeDir: process.env.HOME || os.homedir(),
29
+ pkgDir,
30
+ });
31
+
32
+ process.stdout.write(`${output}\n`);
33
+ process.exitCode = exitCode;
package/package.json CHANGED
@@ -1,8 +1,40 @@
1
1
  {
2
2
  "name": "create-agentic-workspace",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving this name for the npm trusted-publishing bootstrap. Do not install; use the latest version.",
3
+ "version": "0.2.2",
4
+ "description": "The pre-session bootstrap wizard for an Agentic Foundry workspace: declares (never grants) the permission floor, absorbs foundry-bootstrap.sh's out-of-session identity wiring, and scaffolds a seven-file schema-valid workspace. Zero dependencies, no lifecycle scripts, no telemetry.",
5
5
  "license": "MIT",
6
- "bin": { "create-agentic-workspace": "bin.mjs" },
7
- "files": ["bin.mjs", "package.json"]
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lukasrepublic/agentic-foundry.git",
10
+ "directory": "cli"
11
+ },
12
+ "homepage": "https://github.com/lukasrepublic/agentic-foundry/tree/main/cli#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/lukasrepublic/agentic-foundry/issues"
15
+ },
16
+ "bin": {
17
+ "create-agentic-workspace": "bin/create-agentic-workspace.mjs"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.0.0"
21
+ },
22
+ "files": [
23
+ "bin/create-agentic-workspace.mjs",
24
+ "src",
25
+ "templates",
26
+ "permission-floor.json",
27
+ "package.json",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "test": "node --test 'test/**/*.test.mjs'"
32
+ },
33
+ "foundry": {
34
+ "marketplace_name": "agentic-foundry",
35
+ "marketplace_repo": "lukasrepublic/agentic-foundry",
36
+ "plugin_name": "foundry",
37
+ "plugin_version": "1.2.2",
38
+ "pins_researched": "2026-08-02"
39
+ }
8
40
  }
@@ -0,0 +1,411 @@
1
+ {
2
+ "schema_version": 1,
3
+ "plugin_root_glob": "~/.claude/plugins/cache/*/foundry/*",
4
+ "generated_for_plugin_version": "1.2.2",
5
+ "entries": [
6
+ {
7
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-acceptance-contract-validate.py:*)",
8
+ "tier": "allow",
9
+ "rationale": "read-only CLI over foundry_contract; validates/hashes a contract, writes nothing"
10
+ },
11
+ {
12
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-apply-runtime-gitignore.sh:*)",
13
+ "tier": "allow",
14
+ "rationale": "idempotent managed-block writer confined to <repo-root>/.gitignore; refuses on malformed state"
15
+ },
16
+ {
17
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-audit-prepare.py:*)",
18
+ "tier": "allow",
19
+ "rationale": "deterministic host-side Claim-Check binder for the audit engine; read-only path loader"
20
+ },
21
+ {
22
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-audit-record.py:*)",
23
+ "tier": "allow",
24
+ "rationale": "writes one schema-validated evidence row to .foundry/audit-ledger.jsonl at the end of /foundry:audit"
25
+ },
26
+ {
27
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-build-citation-graph.py:*)",
28
+ "tier": "allow",
29
+ "rationale": "builds the derived citation-graph cache from plain-text source-of-truth files"
30
+ },
31
+ {
32
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-coherence-check.py:*)",
33
+ "tier": "allow",
34
+ "rationale": "advisory citation-coherence sweep; builds the graph fresh in-memory, never a merge gate"
35
+ },
36
+ {
37
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-config.py:*)",
38
+ "tier": "allow",
39
+ "rationale": "adopter-config drift check; check writes nothing, adopt writes exactly one baseline file"
40
+ },
41
+ {
42
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-content-conformance.py:*)",
43
+ "tier": "allow",
44
+ "rationale": "declarative content shape/leak checker the operator runs over docs/pack/skill content atoms"
45
+ },
46
+ {
47
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-dashboard-fidelity.py:*)",
48
+ "tier": "allow",
49
+ "rationale": "N/N MATCH fidelity gate between golden dashboards and the code-generated renderer output"
50
+ },
51
+ {
52
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-deploy-status.py:*)",
53
+ "tier": "allow",
54
+ "rationale": "observe-only deployed-artifact-identity cross-check against ArgoCD sync + build provenance"
55
+ },
56
+ {
57
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-distill.py:*)",
58
+ "tier": "allow",
59
+ "rationale": "reads dated learning-capture files and distills them; run directly by the operator per learn-distill"
60
+ },
61
+ {
62
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-doctor.py)",
63
+ "tier": "allow",
64
+ "rationale": "bare invocation runs the thin 5-check health probe; read-only, fail-open; exact rule deliberately — flag-qualified forms fall through to a prompt so no allow-prefix can reach the --heal ceremony (security review 2026-08-02)."
65
+ },
66
+ {
67
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-env-hygiene.py:*)",
68
+ "tier": "allow",
69
+ "rationale": "status/reap over machine-scoped worker sandbox envs; confined to isolated per-worker state"
70
+ },
71
+ {
72
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-fleet-doctor.py:*)",
73
+ "tier": "allow",
74
+ "rationale": "read-only health sweep across all adopter handbooks wiring the plugin on one machine"
75
+ },
76
+ {
77
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-fleet-roster.py:*)",
78
+ "tier": "allow",
79
+ "rationale": "read-only live roster render, one row per native session, via the session-registry"
80
+ },
81
+ {
82
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-fleet-session-machinery.py:*)",
83
+ "tier": "allow",
84
+ "rationale": "derives a typed process-machinery overlay for the fleet surface; read-only"
85
+ },
86
+ {
87
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-fleet-session-registry.py:*)",
88
+ "tier": "allow",
89
+ "rationale": "thin read-only overlay of foundry work-context onto the native session list"
90
+ },
91
+ {
92
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-graph-mcp.py:*)",
93
+ "tier": "allow",
94
+ "rationale": "advisory citation-graph staleness surface / MCP server; run in CONTRIBUTING dev loop + CI"
95
+ },
96
+ {
97
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-index.py:*)",
98
+ "tier": "allow",
99
+ "rationale": "read-only machinery index derived on demand from skills/agents frontmatter"
100
+ },
101
+ {
102
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-native-todo-discipline.py:*)",
103
+ "tier": "allow",
104
+ "rationale": "SessionStart injection directive over the native Tasks list; no external writes"
105
+ },
106
+ {
107
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-prepublication-leak-scan.py:*)",
108
+ "tier": "allow",
109
+ "rationale": "pre-visibility-flip leak scan; read-only denylist/term-matching over the tree"
110
+ },
111
+ {
112
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-project-rtm.py:*)",
113
+ "tier": "allow",
114
+ "rationale": "read-only requirements-traceability-matrix report over committed governance records"
115
+ },
116
+ {
117
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-release-acceptance.py:*)",
118
+ "tier": "allow",
119
+ "rationale": "pre-cut acceptance gate wrapping the tool's own first-party claude plugin validators"
120
+ },
121
+ {
122
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-spec-lint.py:*)",
123
+ "tier": "allow",
124
+ "rationale": "Phase 0 deterministic pre-lint wrapper for /foundry:spec-review; read-only checks"
125
+ },
126
+ {
127
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-stack-profile.py --load:*)",
128
+ "tier": "allow",
129
+ "rationale": "read-only resolve of the already-locked stack-profile.lock"
130
+ },
131
+ {
132
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-stack-profile.py --validate:*)",
133
+ "tier": "allow",
134
+ "rationale": "read-only profile validation against packs/ or a path"
135
+ },
136
+ {
137
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-statusline.sh:*)",
138
+ "tier": "allow",
139
+ "rationale": "read-only, fail-open statusline renderer; prints and exits 0"
140
+ },
141
+ {
142
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-subagent-statusline.sh:*)",
143
+ "tier": "allow",
144
+ "rationale": "read-only, fail-open sub-agent statusline renderer; prints and exits 0"
145
+ },
146
+ {
147
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-verify.py:*)",
148
+ "tier": "allow",
149
+ "rationale": "profile-parameterized static-validation + test executor (SDLC steps 7 & 8); dev-loop CLI"
150
+ },
151
+ {
152
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-wave-plan.py:*)",
153
+ "tier": "allow",
154
+ "rationale": "deterministic release-wave planning over a release manifest; read-only"
155
+ },
156
+ {
157
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-wt:*)",
158
+ "tier": "allow",
159
+ "rationale": "worktree wrapper, workspace-root-confined by construction; consumed by WorktreeCreate + dispatch"
160
+ },
161
+ {
162
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_ceremony_tier.py:*)",
163
+ "tier": "allow",
164
+ "rationale": "one-line ceremony-tier classification printed by /foundry:intake and /foundry:spec-review"
165
+ },
166
+ {
167
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_control_plane.py:*)",
168
+ "tier": "allow",
169
+ "rationale": "read-only control-plane preflight probe run at /foundry:init"
170
+ },
171
+ {
172
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_dispatch_lint.py:*)",
173
+ "tier": "allow",
174
+ "rationale": "advisory context-diet lint over an assembled dispatch prompt; fail-open, flags-only"
175
+ },
176
+ {
177
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_env_isolation.py:*)",
178
+ "tier": "allow",
179
+ "rationale": "read-only per-worker env-isolation check consumed by the env-reap hook"
180
+ },
181
+ {
182
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_grounding_conformance.py:*)",
183
+ "tier": "allow",
184
+ "rationale": "classifies corpus grounding conformance against an already-built system snapshot; read-only"
185
+ },
186
+ {
187
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_intake_grounding.py:*)",
188
+ "tier": "allow",
189
+ "rationale": "read-only intake schema-defect / grounding checker over a project dir"
190
+ },
191
+ {
192
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_repo_fleet.py status:*)",
193
+ "tier": "allow",
194
+ "rationale": "read-only per-repo status rows through the hardened choke point; no network, no mutation."
195
+ },
196
+ {
197
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_repo_fleet.py validate:*)",
198
+ "tier": "allow",
199
+ "rationale": "read-only manifest⟷reality⟷gitignore round-trip incl. the reverse scan; surfaces, never fixes."
200
+ },
201
+ {
202
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_repo_registry.py:*)",
203
+ "tier": "allow",
204
+ "rationale": "Read-only registry-integrity report (feat-foundry-repo-registry-formalization): physical-confinement + gitignore-pairing + origin-match rows over .claude/foundry-project.json; single read-only git choke point, sanitizing emission sink, writes nothing. Tiered at merge-reconciliation when the script landed after the map (the closed-world tripwire firing as designed)."
205
+ },
206
+ {
207
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_run_metrics.py:*)",
208
+ "tier": "allow",
209
+ "rationale": "posttooluse telemetry logger invoked by the run-metrics hook; appends a loss-tracking line"
210
+ },
211
+ {
212
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_session_mode.py:*)",
213
+ "tier": "allow",
214
+ "rationale": "session-scoped mode/fork-policy set-and-resolve state-writer; not a release/repo ceremony"
215
+ },
216
+ {
217
+ "rule": "Bash(claude plugin tag:*)",
218
+ "tier": "ask",
219
+ "rationale": "a tree path arrives between the verb and --push, not prefix-keyable at finer grain; a dry-run also prompts"
220
+ },
221
+ {
222
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-authorize.py:*)",
223
+ "tier": "ask",
224
+ "rationale": "ceremony: front-authorization; --yes/--skip-audit-reason arrive after other args, not prefix-keyable, so the whole script is ask"
225
+ },
226
+ {
227
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-bootstrap.sh:*)",
228
+ "tier": "ask",
229
+ "rationale": "writes GLOBAL git config (--global --unset-all/--get-regexp, two includeIf.*.path writes); machine-scope state"
230
+ },
231
+ {
232
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-cut-release.py:*)",
233
+ "tier": "ask",
234
+ "rationale": "ER class-C release-cut ceremony marker, taken at the coarser fail-safe tier; the script itself mutates no tree"
235
+ },
236
+ {
237
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-decommission.py gate-check:*)",
238
+ "tier": "ask",
239
+ "rationale": "ceremony: leading subcommand, gate-checks a decommission"
240
+ },
241
+ {
242
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-decommission.py record:*)",
243
+ "tier": "ask",
244
+ "rationale": "ceremony: leading subcommand, writes a decommission record"
245
+ },
246
+ {
247
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-doctor.py --heal:*)",
248
+ "tier": "ask",
249
+ "rationale": "pinned ceremony (AC-PFM-3): documented no-op today, but the flag is the shape a future auto-heal would reuse"
250
+ },
251
+ {
252
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-project-sync.py:*)",
253
+ "tier": "ask",
254
+ "rationale": "re-tiered by spec review: non-dry-run path issues authenticated GraphQL write mutations against a remote"
255
+ },
256
+ {
257
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-stack-profile.py --relock:*)",
258
+ "tier": "ask",
259
+ "rationale": "pinned ceremony (AC-PFM-3): re-writes the lock in place; leading flag verified prefix-keyable; the split tier with --validate/--load is safe ONLY because main() checks those flags before --relock — a combined invocation runs the earlier-checked verb (security review 2026-08-02)."
260
+ },
261
+ {
262
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-upstream-submit.py:*)",
263
+ "tier": "ask",
264
+ "rationale": "ceremony: --create arrives after other args, not prefix-keyable, so the whole script is ask"
265
+ },
266
+ {
267
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_release.py accept:*)",
268
+ "tier": "ask",
269
+ "rationale": "ceremony: leading subcommand, appends a PRACTICE acceptance record"
270
+ },
271
+ {
272
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_repo_fleet.py foreach:*)",
273
+ "tier": "ask",
274
+ "rationale": "foreach fans an operator-supplied command over every present repo — arbitrary exec from the agent's hands prompts; the operator's own in-terminal use is unaffected."
275
+ },
276
+ {
277
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_repo_fleet.py sync:*)",
278
+ "tier": "ask",
279
+ "rationale": "sync clones/fetches from manifest-declared remotes — the factory's first network egress; the manifest write is the standing consent, the ask is the per-run operator moment (feat-foundry-workspace-repo-verbs). Subcommand split is reach-disjoint: argparse positional subcommands, first token decides the verb."
280
+ },
281
+ {
282
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_repo_attach.py attach:*)",
283
+ "tier": "ask",
284
+ "rationale": "attach-existing writes the manifest + gitignore pairing and hands the new row to reconcile (clone/fetch egress) - a workspace mutation with network reach prompts per run; the preview is the consent surface (feat-foundry-wizard-attach-repo-flow)."
285
+ },
286
+ {
287
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_repo_attach.py create:*)",
288
+ "tier": "ask",
289
+ "rationale": "create-new invokes `gh repo create` under the operator's identity then falls through to attach - remote resource creation always prompts. Subcommand split is reach-disjoint: argparse positional subcommands, first token decides the verb (feat-foundry-wizard-attach-repo-flow)."
290
+ },
291
+ {
292
+ "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry_tier_preflight.py:*)",
293
+ "tier": "ask",
294
+ "rationale": "--apply POSTs repository rulesets with an Administration (write) token; it writes the merge floor itself"
295
+ },
296
+ {
297
+ "rule": "Bash(docker system prune:*)",
298
+ "tier": "deny",
299
+ "rationale": "absolute anti-pattern: unattended bulk local-state destruction"
300
+ },
301
+ {
302
+ "rule": "Bash(gh pr merge --admin:*)",
303
+ "tier": "deny",
304
+ "rationale": "absolute anti-pattern: bypasses required reviews/checks on the merge floor"
305
+ },
306
+ {
307
+ "rule": "Bash(git push --force:*)",
308
+ "tier": "deny",
309
+ "rationale": "absolute anti-pattern: force-push; belt-and-braces behind the hook-enforced floor (R3)"
310
+ },
311
+ {
312
+ "rule": "Bash(tofu destroy -auto-approve:*)",
313
+ "tier": "deny",
314
+ "rationale": "absolute anti-pattern: unattended destructive infra apply with no plan review"
315
+ }
316
+ ],
317
+ "not_invoked": [
318
+ {
319
+ "script": "foundry_audit_ledger.py",
320
+ "rationale": "library: no argparse/__main__; imported by foundry-audit-record.py for the ledger schema/writer"
321
+ },
322
+ {
323
+ "script": "foundry_audit_log.py",
324
+ "rationale": "library: no argparse/__main__; shared audit-log helper imported by other scripts"
325
+ },
326
+ {
327
+ "script": "foundry_audit_preconditions.py",
328
+ "rationale": "the shipped __main__ is a manual-invocation debug CLI for a human/operator; never instructed by any skill/hook"
329
+ },
330
+ {
331
+ "script": "foundry_authz.py",
332
+ "rationale": "library: no argparse/__main__; imported for authorization/spec-state reads (e.g. foundry_authz.spec_state)"
333
+ },
334
+ {
335
+ "script": "foundry_blueprint.py",
336
+ "rationale": "library: no argparse/__main__; blueprint load/render helpers imported by stack-profile"
337
+ },
338
+ {
339
+ "script": "foundry_contract.py",
340
+ "rationale": "library: no argparse/__main__; acceptance-contract schema/hash helpers imported by other scripts"
341
+ },
342
+ {
343
+ "script": "foundry_ctx_posture.py",
344
+ "rationale": "library: no argparse/__main__; command-policy posture helpers imported by id-* skills"
345
+ },
346
+ {
347
+ "script": "foundry_graph.py",
348
+ "rationale": "library: no argparse/__main__; citation-graph primitives imported by the graph CLIs"
349
+ },
350
+ {
351
+ "script": "foundry_id_alloc.py",
352
+ "rationale": "library: no argparse/__main__; id-allocation helpers imported by id-* skills"
353
+ },
354
+ {
355
+ "script": "foundry_id_apply.py",
356
+ "rationale": "pure decision library: no argparse/main()/__main__; exposes classify_gitops/decide_apply as pure functions (R5)"
357
+ },
358
+ {
359
+ "script": "foundry_permission_floor.py",
360
+ "rationale": "pure library: no argparse/main()/__main__; imported by scripts/foundry-doctor.py's permission-floor probe (feat-foundry-doctor-permission-floor-check), never in command position"
361
+ },
362
+ {
363
+ "script": "foundry_plan_model.py",
364
+ "rationale": "the shipped __main__ is a convenience CLI (parse a plan JSON on stdin); the shipped workflow only imports its parsers"
365
+ },
366
+ {
367
+ "script": "foundry_pr_review.py",
368
+ "rationale": "library: no argparse/__main__; PR-review helpers imported by other scripts"
369
+ },
370
+ {
371
+ "script": "foundry_project_config.py",
372
+ "rationale": "the shipped __main__ prints resolved governance paths for a human; the shipped workflow only imports its reader"
373
+ },
374
+ {
375
+ "script": "foundry_project_tracking.py",
376
+ "rationale": "has an argparse CLI, but it is never instructed by any skill/hook; the workflow only imports read_config"
377
+ },
378
+ {
379
+ "script": "foundry_realization.py",
380
+ "rationale": "library: no argparse/__main__; loads foundry_plan_model.py dynamically for the empties-check"
381
+ },
382
+ {
383
+ "script": "foundry_reconcile.py",
384
+ "rationale": "library: no argparse/__main__; reconciliation helpers imported by other scripts"
385
+ },
386
+ {
387
+ "script": "foundry_sandbox_signature.py",
388
+ "rationale": "library: no argparse/__main__; sandbox-apply signature helpers, dormant per skills/infra-sandboxed-apply"
389
+ },
390
+ {
391
+ "script": "foundry_security_review.py",
392
+ "rationale": "library: no argparse/__main__; security-review helpers imported by other scripts"
393
+ },
394
+ {
395
+ "script": "foundry_system_snapshot.py",
396
+ "rationale": "the shipped __main__/CLI is never instructed; the workflow only imports build_system_snapshot"
397
+ },
398
+ {
399
+ "script": "foundry-statusline-wrapper.sh",
400
+ "rationale": "installed into the adopter repo; the platform's statusLine key runs the installed copy, never an agent Bash call"
401
+ },
402
+ {
403
+ "script": "foundry-subagent-statusline-wrapper.sh",
404
+ "rationale": "installed into the adopter repo; the platform's subagentStatusLine key runs the installed copy, never an agent Bash call"
405
+ },
406
+ {
407
+ "script": "foundry-direnv-lib.sh",
408
+ "rationale": "direnv global-lib file, sourced by direnv, definitions only; not owner-executable, outside the AC-PFM-2 closed world"
409
+ }
410
+ ]
411
+ }
@@ -0,0 +1,68 @@
1
+ // answers.mjs — resolves the final answer set from parsed argv + (optionally) interactive
2
+ // prompts, honoring AC-BCL-2's "yes-mode: zero prompts" invariant.
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { RefusalError } from './util.mjs';
5
+
6
+ function coerceBoolean(raw) {
7
+ const v = String(raw).trim().toLowerCase();
8
+ if (v === '' ) return undefined;
9
+ return v === 'y' || v === 'yes' || v === 'true' || v === '1';
10
+ }
11
+
12
+ /** yesMode is true when --yes was given OR stdin is not a TTY (AC-BCL-2): both suppress every
13
+ * prompt, including the write-phase confirmation (AC-BCL-3). */
14
+ export function isYesMode(values, isTTY) {
15
+ return values.yes === true || !isTTY;
16
+ }
17
+
18
+ /** Resolve every table record to a final value. In yes-mode: apply declared defaults, refusing
19
+ * (naming the flag) on an unanswered required record. Interactively: prompt only records marked
20
+ * `interactive: true` and not already provided by a flag; a non-interactive record always takes
21
+ * its provided value or its default. */
22
+ export async function resolveAnswers(table, parsed, { yesMode, input, output }) {
23
+ const { values, provided } = parsed;
24
+ const resolved = { ...values };
25
+
26
+ const needsPrompt = yesMode
27
+ ? []
28
+ : table.filter((r) => r.interactive && !provided.has(r.id));
29
+
30
+ let rl = null;
31
+ if (needsPrompt.length > 0) {
32
+ rl = createInterface({ input, output });
33
+ }
34
+ try {
35
+ for (const rec of table) {
36
+ if (provided.has(rec.id)) continue;
37
+ if (yesMode || !rec.interactive) {
38
+ if (rec.required) {
39
+ throw new RefusalError(`missing required flag: --${rec.flag}`, rec.flag);
40
+ }
41
+ resolved[rec.id] = rec.default;
42
+ continue;
43
+ }
44
+ const suffix = rec.choices ? ` (${rec.choices.join('/')})` : rec.default !== undefined && rec.default !== '' ? ` [${rec.default}]` : '';
45
+ const answer = (await rl.question(`${rec.prompt}${suffix}: `)).trim();
46
+ if (rec.type === 'boolean') {
47
+ const b = coerceBoolean(answer);
48
+ resolved[rec.id] = b === undefined ? rec.default : b;
49
+ } else if (answer === '') {
50
+ if (rec.required) {
51
+ throw new RefusalError(`missing required flag: --${rec.flag}`, rec.flag);
52
+ }
53
+ resolved[rec.id] = rec.default;
54
+ } else {
55
+ if (rec.choices && !rec.choices.includes(answer)) {
56
+ throw new RefusalError(
57
+ `--${rec.flag} must be one of: ${rec.choices.join(', ')} (got ${JSON.stringify(answer)})`,
58
+ rec.flag,
59
+ );
60
+ }
61
+ resolved[rec.id] = answer;
62
+ }
63
+ }
64
+ } finally {
65
+ if (rl) rl.close();
66
+ }
67
+ return resolved;
68
+ }