create-agentic-workspace 0.2.1 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agentic-workspace",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
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
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "marketplace_name": "agentic-foundry",
35
35
  "marketplace_repo": "lukasrepublic/agentic-foundry",
36
36
  "plugin_name": "foundry",
37
- "plugin_version": "1.2.1",
37
+ "plugin_version": "1.3.0",
38
38
  "pins_researched": "2026-08-02"
39
39
  }
40
40
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "plugin_root_glob": "~/.claude/plugins/cache/*/foundry/*",
4
- "generated_for_plugin_version": "1.2.1",
4
+ "generated_for_plugin_version": "1.3.0",
5
5
  "entries": [
6
6
  {
7
7
  "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-acceptance-contract-validate.py:*)",
package/src/answers.mjs CHANGED
@@ -9,6 +9,45 @@ function coerceBoolean(raw) {
9
9
  return v === 'y' || v === 'yes' || v === 'true' || v === '1';
10
10
  }
11
11
 
12
+ /** The bracketed default (AC-WPD-5/-6/-7). A boolean renders `[y]`/`[n]` rather than its literal
13
+ * value — `[false]` reads as a type, not an affordance, on a yes/no question. An empty-string
14
+ * default renders `[blank]`: it states positively that a default exists, that it is blank, and
15
+ * therefore that Enter is safe. Rendering `[]` would read as noise instead. */
16
+ export function defaultToken(rec) {
17
+ if (rec.default === undefined) return '';
18
+ if (rec.type === 'boolean') return rec.default ? '[y]' : '[n]';
19
+ return rec.default === '' ? '[blank]' : `[${rec.default}]`;
20
+ }
21
+
22
+ /** `(choices) [default]` — both when both are declared (AC-WPD-4). A boolean carries the implicit
23
+ * `(y/n)` vocabulary (AC-WPD-7). */
24
+ export function promptSuffix(rec) {
25
+ const parts = [];
26
+ if (rec.choices) parts.push(`(${rec.choices.join('/')})`);
27
+ else if (rec.type === 'boolean') parts.push('(y/n)');
28
+ const dflt = defaultToken(rec);
29
+ if (dflt) parts.push(dflt);
30
+ return parts.length ? ` ${parts.join(' ')}` : '';
31
+ }
32
+
33
+ /** The description block printed ABOVE the prompt line (AC-WPD-2), with one indented line per
34
+ * enumerated choice BETWEEN the description and the prompt (AC-WPD-3). Derived from the question
35
+ * table and from no second list (AC-WPD-12). */
36
+ export function renderPromptBlock(rec) {
37
+ // Leading blank line: readline echoes no newline after a piped answer, so without this the next
38
+ // question's description starts on the same line as the previous prompt. Separating the blocks
39
+ // is also simply easier to read in a real terminal.
40
+ const lines = ['', ...String(rec.description).split('\n')];
41
+ if (rec.choices) {
42
+ const width = Math.max(...rec.choices.map((c) => c.length));
43
+ for (const c of rec.choices) {
44
+ const note = (rec.choiceDescriptions || {})[c] || '';
45
+ lines.push(` ${c.padEnd(width)} ${note}`.trimEnd());
46
+ }
47
+ }
48
+ return lines.join('\n') + '\n';
49
+ }
50
+
12
51
  /** yesMode is true when --yes was given OR stdin is not a TTY (AC-BCL-2): both suppress every
13
52
  * prompt, including the write-phase confirmation (AC-BCL-3). */
14
53
  export function isYesMode(values, isTTY) {
@@ -41,8 +80,11 @@ export async function resolveAnswers(table, parsed, { yesMode, input, output })
41
80
  resolved[rec.id] = rec.default;
42
81
  continue;
43
82
  }
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();
83
+ // The suffix is ADDITIVE (AC-WPD-4): choices and default render TOGETHER. The prior
84
+ // mutually-exclusive ternary suppressed the default on every record declaring choices —
85
+ // structurally, so the one record where a default matters most could never show it.
86
+ output.write(renderPromptBlock(rec));
87
+ const answer = (await rl.question(`${rec.prompt}${promptSuffix(rec)}: `)).trim();
46
88
  if (rec.type === 'boolean') {
47
89
  const b = coerceBoolean(answer);
48
90
  resolved[rec.id] = b === undefined ? rec.default : b;
package/src/argv.mjs CHANGED
@@ -60,6 +60,19 @@ export function renderHelp(table, { programName = 'create-agentic-workspace' } =
60
60
  const req = rec.required ? ' (required)' : rec.default !== undefined && rec.default !== '' ? ` (default: ${rec.default})` : '';
61
61
  const choices = rec.choices ? ` [choices: ${rec.choices.join('|')}]` : '';
62
62
  lines.push(` ${flagCol.padEnd(24)} ${rec.prompt}${choices}${req}`);
63
+ // The description renders as an indented continuation beneath its flag line (AC-WPD-10),
64
+ // across as many physical lines as the description itself contains, then one further
65
+ // indented line per declared choice (AC-WPD-11). Same table, no second list (AC-WPD-12).
66
+ for (const dline of String(rec.description).split('\n')) {
67
+ lines.push(dline ? `${' '.repeat(27)}${dline}` : '');
68
+ }
69
+ if (rec.choices) {
70
+ const width = Math.max(...rec.choices.map((c) => c.length));
71
+ for (const c of rec.choices) {
72
+ const note = (rec.choiceDescriptions || {})[c] || '';
73
+ lines.push(`${' '.repeat(27)} ${c.padEnd(width)} ${note}`.trimEnd());
74
+ }
75
+ }
63
76
  }
64
77
  lines.push('');
65
78
  lines.push('This CLI collects and transmits nothing: no telemetry.');
package/src/preview.mjs CHANGED
@@ -23,12 +23,29 @@ export function renderPreview({ plan, machineScopeWrites, map }) {
23
23
  return lines.join('\n');
24
24
  }
25
25
 
26
- export const TRUST_HANDOFF_TEXT = (dir) => `
26
+ // `isGitRepo` is computed by the CALLER and passed in, so this module stays pure (it renders text;
27
+ // it does not probe the filesystem).
28
+ //
29
+ // WHY THE GIT STEP IS PRINTED AND NOT PERFORMED. A workspace that is not a git repository is a
30
+ // dead end: the factory's whole discipline is branch-per-atom → PR → merge floor, and the setup
31
+ // path tells the reader to apply branch protection a few steps later — which needs a repo AND a
32
+ // remote. This CLI ALREADY writes a `.gitignore`, which means nothing without one.
33
+ //
34
+ // It is still printed rather than run. This CLI's contract is that it writes exactly the files it
35
+ // previewed and nothing else; `git init` would be an un-previewed side effect, and the choice of
36
+ // remote is genuinely the reader's. `create-vite` prints its next steps the same way, for the same
37
+ // reason. Telling keeps the promise; doing would quietly break it.
38
+ export const TRUST_HANDOFF_TEXT = (dir, { isGitRepo = true } = {}) => `
27
39
  The workspace has been written. The permission floor above is a declaration, not a grant: the
28
40
  platform's trust dialog is the consent ceremony, and the \`allow\` rules take effect only after
29
41
  you accept it — the dialog lists them.
30
42
 
31
- cd ${dir}
43
+ cd ${dir}${isGitRepo ? '' : `
44
+ git init && git add -A && git commit -m 'workspace seed'
45
+ # ^ this directory is NOT a git repository yet. The factory works through
46
+ # branch-per-atom → PR → the merge floor, so it needs a repo and a remote.
47
+ # Create the remote too (e.g. \`gh repo create <you>/<project>-handbook --private --source=. --push\`)
48
+ # before you reach the branch-protection step.`}
32
49
  claude
33
50
  > (accept the trust dialog when prompted)
34
51
  > /foundry:init
package/src/questions.mjs CHANGED
@@ -1,8 +1,20 @@
1
1
  // questions.mjs — THE single question table (AC-BCL-2). Every accepted long flag, every --help
2
2
  // line and every interactive prompt are derived from this table and from no second list.
3
3
  //
4
- // Record shape: { id, flag, type: 'string'|'boolean', prompt, default? , required?, choices?,
5
- // interactive } — `default` XOR `required: true` (never both, never neither).
4
+ // Record shape: { id, flag, type: 'string'|'boolean', prompt, description, default? , required?,
5
+ // choices?, choiceDescriptions?, writesOutsideProject?, interactive }
6
+ // `default` XOR `required: true` (never both, never neither).
7
+ //
8
+ // `description` (AC-WPD-1) is prose stating what the answer DOES — rendered above the interactive
9
+ // prompt (AC-WPD-2) and as an indented continuation in --help (AC-WPD-10). `choiceDescriptions`
10
+ // carries one line per enumerated value (AC-WPD-3/-11); `choices` stays a plain string[] so
11
+ // argv.mjs's membership check and the frozen AC-BCL-2 refusal test are untouched.
12
+ //
13
+ // `writesOutsideProject` (AC-WPD-9) marks a record whose answer writes outside the target root.
14
+ // Its description MUST state the scope of the effect AND name each such artifact (AC-WPD-8/-14).
15
+ // Naming the file without naming the scope is the failure this annotation exists to prevent: the
16
+ // draft copy ("writes global git config, machine-wide") was read by the framework's own operator
17
+ // as changing their global identity, which it does not do.
6
18
 
7
19
  export const QUESTION_TABLE = [
8
20
  {
@@ -10,6 +22,11 @@ export const QUESTION_TABLE = [
10
22
  flag: 'dir',
11
23
  type: 'string',
12
24
  prompt: 'Workspace directory to create',
25
+ description:
26
+ 'The folder your specs, config, and factory wiring will live in — one\n' +
27
+ 'per project (e.g. ./my-project-handbook). Relative to where you are\n' +
28
+ 'now, or an absolute path.\n' +
29
+ 'Required — there is no default, so Enter alone will not do.',
13
30
  required: true,
14
31
  interactive: true,
15
32
  },
@@ -17,7 +34,11 @@ export const QUESTION_TABLE = [
17
34
  id: 'existing',
18
35
  flag: 'existing',
19
36
  type: 'boolean',
20
- prompt: 'Target directory already exists and is non-empty (scaffold into it; never clobbers)',
37
+ prompt: 'Scaffold into an existing folder?',
38
+ description:
39
+ 'Nothing already there is overwritten — existing files are left alone and\n' +
40
+ 'only the missing pieces are added.\n' +
41
+ 'Enter alone means no. y, yes, true or 1 mean yes.',
21
42
  default: false,
22
43
  interactive: true,
23
44
  },
@@ -26,23 +47,53 @@ export const QUESTION_TABLE = [
26
47
  flag: 'stage-mode',
27
48
  type: 'string',
28
49
  prompt: 'Stage mode',
50
+ description:
51
+ 'How much process ceremony this workspace enforces.\n' +
52
+ "Recorded in your workspace's CLAUDE.md; agents read it each session,\n" +
53
+ 'and you can change it later by editing that line.',
29
54
  default: 'lean',
30
55
  choices: ['lean', 'scale'],
56
+ choiceDescriptions: {
57
+ lean: 'development loop with lighter ceremony — the solo/small-team norm',
58
+ scale: 'higher ceremony loop enforced end to end',
59
+ },
31
60
  interactive: true,
32
61
  },
33
62
  {
34
63
  id: 'ghAccount',
35
64
  flag: 'gh-account',
36
65
  type: 'string',
37
- prompt: 'GitHub account slug for commit-identity isolation (blank to skip)',
66
+ prompt: 'GitHub account slug (blank to skip)',
67
+ description:
68
+ 'GitHub account for commit identity.\n' +
69
+ 'On a machine with more than one GitHub account, this makes sure commits\n' +
70
+ 'in THIS workspace are always authored by the right one.\n' +
71
+ '\n' +
72
+ ' Leave blank to skip. Nothing outside this folder is written.\n' +
73
+ '\n' +
74
+ ' If you enter a slug, your global git identity is NOT changed. Git is\n' +
75
+ ' taught a rule that applies ONLY inside this folder:\n' +
76
+ ' - a small identity file is created at ~/.config/git/identity-<slug>\n' +
77
+ ' - two "only inside this folder" rules are added to your ~/.gitconfig,\n' +
78
+ ' pointing at that file\n' +
79
+ ' - inside this repo, git is told never to guess an identity, so a\n' +
80
+ ' mistake fails loudly instead of committing as the wrong you',
38
81
  default: '',
82
+ writesOutsideProject: true,
39
83
  interactive: true,
40
84
  },
41
85
  {
42
86
  id: 'gitAuthor',
43
87
  flag: 'git-author',
44
88
  type: 'string',
45
- prompt: 'Git author "Name <email>" (blank: try gh, then prompt, then fail closed — only used with --gh-account)',
89
+ prompt: 'Git author name and email (blank to look up)',
90
+ description:
91
+ 'Name and email to record in that identity file.\n' +
92
+ 'Only used if you entered an account slug above — otherwise ignored.\n' +
93
+ 'Format: Name <email@example.com>\n' +
94
+ 'Leave blank to look it up from your GitHub account. If that cannot be\n' +
95
+ "determined you'll be asked; if it still cannot be resolved the run stops\n" +
96
+ 'rather than committing under a guessed identity.',
46
97
  default: '',
47
98
  interactive: true,
48
99
  },
@@ -50,7 +101,12 @@ export const QUESTION_TABLE = [
50
101
  id: 'yes',
51
102
  flag: 'yes',
52
103
  type: 'boolean',
53
- prompt: 'Accept every default and the write-phase confirmation without prompting',
104
+ prompt: 'Accept every default and skip the confirmation',
105
+ description:
106
+ 'Non-interactive mode: every question resolves to its\n' +
107
+ 'default and the final write confirmation is skipped.\n' +
108
+ 'Required flags must be given on the command line or the\n' +
109
+ 'run refuses. Also implied when stdin is not a terminal.',
54
110
  default: false,
55
111
  interactive: false,
56
112
  },
@@ -58,7 +114,11 @@ export const QUESTION_TABLE = [
58
114
  id: 'dryRun',
59
115
  flag: 'dry-run',
60
116
  type: 'boolean',
61
- prompt: 'Print the preview only; make no write and no side effect',
117
+ prompt: 'Print the preview only; make no write',
118
+ description:
119
+ 'Shows exactly what would be written — files inside the\n' +
120
+ 'workspace and any machine-scope writes outside it — then\n' +
121
+ 'exits. Nothing is created; no child process is spawned.',
62
122
  default: false,
63
123
  interactive: false,
64
124
  },
@@ -67,6 +127,9 @@ export const QUESTION_TABLE = [
67
127
  flag: 'help',
68
128
  type: 'boolean',
69
129
  prompt: 'Show this help and exit',
130
+ description:
131
+ 'Prints every option with the same explanation the\n' +
132
+ 'interactive prompt shows.',
70
133
  default: false,
71
134
  interactive: false,
72
135
  },
package/src/run.mjs CHANGED
@@ -194,7 +194,12 @@ export async function runCli(argv, { cwd, isTTY, input, output, homeDir, pkgDir
194
194
  for (const f of findings) print(` [${f.class}] ${JSON.stringify(f)}`);
195
195
  }
196
196
 
197
- print(TRUST_HANDOFF_TEXT(targetRoot));
197
+ // Probed here, not inside preview.mjs, which renders text and never touches the filesystem.
198
+ // `.git` is a DIRECTORY in a normal clone and a FILE in a worktree/submodule, so `existsSync`
199
+ // on the path is the check that covers both — a directory-only test would tell a worktree user
200
+ // to re-init a repository they already have.
201
+ const isGitRepo = fs.existsSync(path.join(targetRoot, '.git'));
202
+ print(TRUST_HANDOFF_TEXT(targetRoot, { isGitRepo }));
198
203
 
199
204
  return { exitCode: exitCodeForPlan(plan), output: lines.join('\n') };
200
205
  } catch (e) {