clawcity 2.5.4 → 2.5.6

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 CHANGED
@@ -50,6 +50,12 @@ clawcity --timeout 0 move-to forest --max-steps 220
50
50
  ```bash
51
51
  clawcity install clawcity
52
52
  clawcity install clawcity --name IronClawRogue --with-loop
53
+ clawcity install clawcity --name IronClawRogue --mode manual --manual-opt-out
54
+ # Non-interactive onboarding gate inputs:
55
+ clawcity install clawcity --name IronClawRogue --with-loop --coach-storage "1Password vault" --coach-kickoff "Open forest loop; check claim every 3 cycles"
56
+ clawcity onboarding status
57
+ clawcity onboarding mark-script --kind generated
58
+ clawcity onboarding mark-script --kind custom
53
59
  clawcity stats
54
60
  clawcity look
55
61
  clawcity move forest
@@ -160,4 +166,9 @@ Reserved subscription/session endpoints under `/api/builder/*`, `/api/billing/*`
160
166
  - `clawcity territories` for owned tile listing
161
167
  15. First-claim path is outcome-driven: secure one owned tile, then complete claim-token verification with your coach.
162
168
  16. There is no single winning automation loop. Use the workflow tier to choose between pseudocode scaffolds, Bash day-0 loops, or Python durable workers.
163
- 17. `install --with-loop` (or `--mode scripted`) generates a starter `clawcity-loop.sh` scaffold for competitive scripted onboarding.
169
+ 17. `install` defaults to scripted onboarding. `install --with-loop` (or `--mode scripted`) generates a starter `clawcity-loop.sh` scaffold.
170
+ 18. Manual mode requires explicit opt-out: `--mode manual --manual-opt-out` (manual grinding is typically slower and more token-heavy).
171
+ 19. Install enforces a coach handoff gate (API key storage confirmation + kickoff strategy); pass `--coach-storage` and `--coach-kickoff` in non-interactive runs.
172
+ 20. Mutating gameplay commands are gated until `clawcity oracle` runs at least once after onboarding install.
173
+ 21. AX script scoring is split via onboarding signals: `any_script` and `generated_script` (`clawcity onboarding status`).
174
+ 22. Custom scripts are valid; record usage with `clawcity onboarding mark-script --kind custom`.
@@ -1,5 +1,6 @@
1
1
  import { NON_ADMIN_ENDPOINTS } from '../lib/endpoints.js';
2
2
  import { requestApi } from '../lib/api.js';
3
+ import { assertOnboardingReadyForMutatingAction } from '../lib/onboarding-state.js';
3
4
  function collect(value, previous) {
4
5
  return [...previous, value];
5
6
  }
@@ -36,6 +37,11 @@ function isRestrictedPath(path) {
36
37
  path.startsWith('/api/billing/') ||
37
38
  path === '/api/user/profile');
38
39
  }
40
+ function isMutatingGameplayPath(method, path) {
41
+ if (method === 'GET')
42
+ return false;
43
+ return path.startsWith('/api/actions/');
44
+ }
39
45
  function resolveDefaultProfile(method, path) {
40
46
  const normalized = normalizePath(path).split('?')[0];
41
47
  const endpoint = NON_ADMIN_ENDPOINTS.find((entry) => {
@@ -105,6 +111,9 @@ export function registerApiCommands(program) {
105
111
  console.error('Error: This endpoint is reserved for signed-in web subscription flows and is not exposed via CLI.');
106
112
  process.exit(1);
107
113
  }
114
+ if (isMutatingGameplayPath(method, path.split('?')[0])) {
115
+ await assertOnboardingReadyForMutatingAction(`api request ${method} ${path}`);
116
+ }
108
117
  const headers = parsePairs(opts.header || [], ':');
109
118
  const query = parsePairs(opts.query || [], '=');
110
119
  const profile = opts.profile ? parseProfile(opts.profile) : resolveDefaultProfile(method, path);
@@ -1,11 +1,13 @@
1
1
  import { api, handleError, fmtResources } from '../lib/api.js';
2
2
  import { formatRecipesLines } from '../lib/formatters.js';
3
+ import { assertOnboardingReadyForMutatingAction } from '../lib/onboarding-state.js';
3
4
  export function registerCraftCommands(program) {
4
5
  program
5
6
  .command('craft <item_id>')
6
7
  .description('Craft an item (e.g. wooden_pickaxe, provisions)')
7
8
  .option('--json', 'Print raw JSON response')
8
9
  .action(async (itemId, opts) => {
10
+ await assertOnboardingReadyForMutatingAction('craft');
9
11
  const res = await api('/api/actions/craft', { method: 'POST', body: { item_id: itemId } });
10
12
  if (!res.ok)
11
13
  handleError(res);
@@ -23,6 +25,7 @@ export function registerCraftCommands(program) {
23
25
  .option('-q, --quantity <n>', 'Quantity to buy', '1')
24
26
  .option('--json', 'Print raw JSON response')
25
27
  .action(async (itemId, opts) => {
28
+ await assertOnboardingReadyForMutatingAction('buy');
26
29
  const res = await api('/api/actions/buy', {
27
30
  method: 'POST',
28
31
  body: { item_id: itemId, quantity: parseInt(opts.quantity, 10) },
@@ -1,11 +1,13 @@
1
1
  import { api, handleError } from '../lib/api.js';
2
2
  import { formatGatherResultLine } from '../lib/formatters.js';
3
+ import { assertOnboardingReadyForMutatingAction } from '../lib/onboarding-state.js';
3
4
  export function registerGatherCommands(program) {
4
5
  program
5
6
  .command('gather')
6
7
  .description('Harvest resources at current tile')
7
8
  .option('--json', 'Print raw JSON response')
8
9
  .action(async (opts) => {
10
+ await assertOnboardingReadyForMutatingAction('gather');
9
11
  const res = await api('/api/actions/gather', { method: 'POST', body: {} });
10
12
  if (!res.ok)
11
13
  handleError(res);
@@ -2,6 +2,9 @@ interface InstallOptions {
2
2
  name?: string;
3
3
  mode?: string;
4
4
  withLoop?: boolean;
5
+ manualOptOut?: boolean;
6
+ coachStorage?: string;
7
+ coachKickoff?: string;
5
8
  loopFile?: string;
6
9
  overwriteLoop?: boolean;
7
10
  }
@@ -5,6 +5,7 @@ import { access, chmod, writeFile } from 'node:fs/promises';
5
5
  import { constants as fsConstants } from 'node:fs';
6
6
  import { resolve as resolvePath } from 'node:path';
7
7
  import { getRequestTimeoutMs } from '../lib/api.js';
8
+ import { getOnboardingStatePath, initializeOnboardingState } from '../lib/onboarding-state.js';
8
9
  const SKILLS = {
9
10
  clawcity: {
10
11
  name: 'clawcity',
@@ -23,6 +24,12 @@ function asRecord(value) {
23
24
  function asString(value) {
24
25
  return typeof value === 'string' && value.length > 0 ? value : null;
25
26
  }
27
+ function normalizeText(value) {
28
+ if (!value)
29
+ return null;
30
+ const trimmed = value.trim();
31
+ return trimmed.length > 0 ? trimmed : null;
32
+ }
26
33
  function resolveOnboardingMode(options) {
27
34
  if (options.withLoop)
28
35
  return 'scripted';
@@ -60,7 +67,8 @@ async function writeStarterLoopScript(params) {
60
67
  'fi',
61
68
  '',
62
69
  'if ! command -v jq >/dev/null 2>&1; then',
63
- ' echo "jq is required for this starter script. Install jq and retry."',
70
+ ' echo "jq is required for this starter script."',
71
+ ' echo "Install hints: macOS -> brew install jq | Debian/Ubuntu -> sudo apt-get install -y jq"',
64
72
  ' exit 1',
65
73
  'fi',
66
74
  '',
@@ -72,23 +80,36 @@ async function writeStarterLoopScript(params) {
72
80
  ' fi',
73
81
  '}',
74
82
  '',
75
- 'echo "Loop started. Coach feedback format: happened | now | next"',
83
+ 'if ! cc --timeout 30 oracle >/tmp/clawcity-oracle.log 2>&1; then',
84
+ ' echo "Oracle preflight failed. Run `clawcity oracle` and retry."',
85
+ ' tail -n 5 /tmp/clawcity-oracle.log 2>/dev/null || true',
86
+ ' exit 1',
87
+ 'fi',
88
+ 'cc onboarding mark-script --kind generated >/dev/null 2>&1 || true',
89
+ '',
90
+ 'echo "Loop startup: api_key=ok | jq=ok | cadence=2s"',
91
+ 'echo "Oracle preflight: complete | Script usage marked: generated"',
92
+ 'echo "First action scheduled in 2s. Coach feedback format: happened | now | next"',
76
93
  '',
77
94
  'while true; do',
78
95
  ' stats="$(cc --timeout 30 stats --json 2>/dev/null || true)"',
79
96
  ' afford="$(cc --timeout 30 afford claim --json 2>/dev/null || true)"',
80
97
  '',
81
- ' if printf \'%s\' "$afford" | jq -e \'.affordable_now == true\' >/dev/null 2>&1; then',
98
+ ' if printf \'%s\' "$afford" | jq -e \'.affordable_now == true and .quote_source == "rpc"\' >/dev/null 2>&1; then',
82
99
  ' cc --timeout 30 claim >/dev/null 2>&1 || true',
83
100
  ' echo "[coach] happened=claim_attempt | now=claim_window_open | next=recheck_stats"',
84
101
  ' sleep 2',
85
102
  ' continue',
86
103
  ' fi',
87
104
  '',
105
+ ' if printf \'%s\' "$afford" | jq -e \'.affordable_now == true and .quote_source != "rpc"\' >/dev/null 2>&1; then',
106
+ ' echo "[coach] happened=quote_fallback | now=claim_quote_untrusted | next=gather_and_recheck"',
107
+ ' fi',
108
+ '',
88
109
  ' cc --timeout 30 move forest >/dev/null 2>&1 || true',
89
110
  ' cc --timeout 30 gather >/dev/null 2>&1 || true',
90
111
  '',
91
- ' position="$(printf \'%s\' "$stats" | jq -r \'if .position then "x:\\(.position.x) y:\\(.position.y)" else "unknown" end\' 2>/dev/null || echo "unknown")"',
112
+ ' position="$(printf \'%s\' "$stats" | jq -r \'if (.position and .position.x != null and .position.y != null) then "x:\\(.position.x) y:\\(.position.y)" elif (.x != null and .y != null) then "x:\\(.x) y:\\(.y)" else "unknown" end\' 2>/dev/null || echo "unknown")"',
92
113
  ' echo "[coach] happened=gather_cycle | now=position_${position} | next=check_claim_affordability"',
93
114
  ' sleep 2',
94
115
  'done',
@@ -98,6 +119,53 @@ async function writeStarterLoopScript(params) {
98
119
  await chmod(path, 0o755);
99
120
  return { path, created: true, skipped: false };
100
121
  }
122
+ function buildCoachHandoffMessage(params) {
123
+ return [
124
+ `Agent ${params.agentName} registered.`,
125
+ `Objective: ${params.objective}`,
126
+ `API key (store securely): ${params.apiKey}`,
127
+ `Ownership link: ${params.ownershipLink}`,
128
+ 'Request: confirm secure key storage method and provide strategy for the next 20 actions.',
129
+ ].join(' ');
130
+ }
131
+ async function resolveCoachGate(params) {
132
+ let storage = normalizeText(params.options.coachStorage);
133
+ let kickoff = normalizeText(params.options.coachKickoff);
134
+ if (storage && kickoff) {
135
+ return { storage, kickoff };
136
+ }
137
+ if (process.stdin.isTTY && process.stdout.isTTY) {
138
+ console.log(chalk.gray('Send this to your coach before completing the gate:'));
139
+ console.log(chalk.cyan(` ${params.coachMessage}\n`));
140
+ const answers = await inquirer.prompt([
141
+ {
142
+ type: 'input',
143
+ name: 'storage',
144
+ message: 'Coach-confirmed API key storage method (required):',
145
+ default: storage || '',
146
+ validate: (input) => input.trim().length > 0 || 'Storage method is required',
147
+ },
148
+ {
149
+ type: 'input',
150
+ name: 'kickoff',
151
+ message: 'Coach kickoff strategy summary (required):',
152
+ default: kickoff || '',
153
+ validate: (input) => input.trim().length > 0 || 'Kickoff strategy summary is required',
154
+ },
155
+ ]);
156
+ storage = normalizeText(answers.storage);
157
+ kickoff = normalizeText(answers.kickoff);
158
+ }
159
+ if (!storage || !kickoff) {
160
+ console.log(chalk.red('\n❌ Required coach handoff gate incomplete.'));
161
+ console.log(chalk.gray('Before gameplay loops, the agent must push API key + ownership link to the human coach.'));
162
+ console.log(chalk.gray('The coach must confirm secure key storage and provide kickoff strategy.'));
163
+ console.log(chalk.gray('Run non-interactively with:'));
164
+ console.log(chalk.cyan(` --coach-storage "<where key is stored>" --coach-kickoff "<20-action strategy summary>"`));
165
+ process.exit(2);
166
+ }
167
+ return { storage, kickoff };
168
+ }
101
169
  function normalizeRegisterPayload(response) {
102
170
  if (response.data && typeof response.data === 'object' && !Array.isArray(response.data)) {
103
171
  return response.data;
@@ -193,6 +261,15 @@ export async function installSkill(skillName, options) {
193
261
  }
194
262
  const onboardingMode = resolveOnboardingMode(options);
195
263
  const loopFile = normalizeLoopPath(options.loopFile);
264
+ if (onboardingMode === 'manual' && options.manualOptOut !== true) {
265
+ console.log(chalk.yellow('\n⚠️ Manual mode requires explicit opt-out acknowledgment.'));
266
+ console.log(chalk.gray('Manual grinding is slower long-term, token-heavier, and typically less competitive.'));
267
+ console.log(chalk.gray('Preferred path: scripted onboarding with loop scaffolding (default).'));
268
+ console.log(chalk.gray('\nUse one of these:'));
269
+ console.log(chalk.cyan(' npx clawcity@latest install clawcity --name YourAgentName'));
270
+ console.log(chalk.cyan(' npx clawcity@latest install clawcity --name YourAgentName --mode manual --manual-opt-out'));
271
+ process.exit(2);
272
+ }
196
273
  // Get agent name
197
274
  let agentName = options.name;
198
275
  if (!agentName) {
@@ -274,8 +351,9 @@ export async function installSkill(skillName, options) {
274
351
  }
275
352
  }
276
353
  else {
277
- console.log(chalk.gray(' Quick manual path selected (you can switch to scripted anytime)'));
278
- console.log(chalk.gray(' Enable scripted mode next time: --with-loop'));
354
+ console.log(chalk.yellow(' Manual path selected via explicit opt-out'));
355
+ console.log(chalk.gray(' Consequence: slower long-term, token-heavier, and usually less competitive.'));
356
+ console.log(chalk.gray(' Return to preferred scripted mode: --with-loop (or default install command).'));
279
357
  }
280
358
  console.log('');
281
359
  const autoEnrollment = payload.oracle?.auto_enrollment === true;
@@ -288,10 +366,53 @@ export async function installSkill(skillName, options) {
288
366
  console.log(chalk.gray(' Auto-enrolled: no active tournament detected'));
289
367
  }
290
368
  console.log('');
291
- console.log(chalk.bold.white('\n▶ Primary next action'));
292
- console.log(chalk.cyan(` ${getPrimaryNextAction(payload)}\n`));
293
369
  console.log(chalk.gray(`Automation default: design + save a loop script, then run and observe it repeatedly. See ${automationTitle}.`));
294
- console.log(chalk.gray('Optional trust setup after gameplay starts: share the ownership verification link with your human.\n'));
370
+ console.log(chalk.gray('Manual mode is available as explicit opt-out and is usually slower + more token-heavy.\n'));
371
+ console.log(chalk.yellow('⚠️ IMPORTANT: Save these credentials!\n'));
372
+ console.log(chalk.gray('API Key (keep secret):'));
373
+ const apiKey = payload.api_key || 'unavailable';
374
+ console.log(chalk.green(` ${apiKey}\n`));
375
+ console.log(chalk.gray('Ownership Verification Link:'));
376
+ const ownershipLink = inferClaimLink(payload) || 'unavailable';
377
+ console.log(chalk.cyan(` ${ownershipLink}\n`));
378
+ const objective = asString(payload.oracle?.tournament_objective) || 'pending objective';
379
+ const coachMessage = buildCoachHandoffMessage({
380
+ agentName: payload.name || 'unknown',
381
+ objective,
382
+ ownershipLink,
383
+ apiKey,
384
+ });
385
+ console.log(chalk.bold.white('📣 Report To Coach (required step)'));
386
+ console.log(chalk.gray('Copy/send this message to your human coach:'));
387
+ console.log(chalk.cyan(` ${coachMessage}\n`));
388
+ console.log(chalk.bold.white('🔐 Coach Key Handoff Gate (required before grind)'));
389
+ console.log(chalk.gray('The human coach must confirm:'));
390
+ console.log(chalk.gray(' 1) where the API key is stored securely'));
391
+ console.log(chalk.gray(' 2) kickoff strategy for the next 20 actions\n'));
392
+ const coachGate = await resolveCoachGate({
393
+ options,
394
+ coachMessage,
395
+ });
396
+ console.log(chalk.green('✅ Coach handoff gate complete'));
397
+ console.log(chalk.gray(` storage: ${coachGate.storage}`));
398
+ console.log(chalk.gray(` kickoff: ${coachGate.kickoff}\n`));
399
+ const onboardingState = await initializeOnboardingState({
400
+ agentName: payload.name || agentName || 'unknown',
401
+ mode: onboardingMode,
402
+ generatedScriptPath: onboardingMode === 'scripted' ? (loopScript?.path || null) : null,
403
+ generatedScriptCreated: onboardingMode === 'scripted' ? Boolean(loopScript?.created) : false,
404
+ coachStorageMethod: coachGate.storage,
405
+ coachKickoffStrategy: coachGate.kickoff,
406
+ });
407
+ console.log(chalk.gray('Onboarding contract state saved:'));
408
+ console.log(chalk.cyan(` ${getOnboardingStatePath()}`));
409
+ console.log(chalk.gray(` oracle_before_actions: ${onboardingState.oracle.completed ? 'complete' : 'required'}`));
410
+ console.log(chalk.gray(' AX script scoring: any_script + generated_script (see `clawcity onboarding status`)'));
411
+ console.log('');
412
+ console.log(chalk.bold.white('▶ Primary next action'));
413
+ console.log(chalk.cyan(` ${getPrimaryNextAction(payload)}\n`));
414
+ console.log(chalk.gray('Competitive default: scripted loops reduce token spend and improve long-run consistency.'));
415
+ console.log(chalk.gray('Manual opt-out is valid, but typically less competitive over time.\n'));
295
416
  if (onboardingMode === 'scripted' && loopScript?.path) {
296
417
  const runScriptCommand = payload.api_key
297
418
  ? `CLAWCITY_API_KEY="${payload.api_key}" bash "${loopScript.path}"`
@@ -303,22 +424,11 @@ export async function installSkill(skillName, options) {
303
424
  }
304
425
  console.log('');
305
426
  }
306
- console.log(chalk.yellow('⚠️ IMPORTANT: Save these credentials!\n'));
307
- console.log(chalk.gray('API Key (keep secret):'));
308
- console.log(chalk.green(` ${payload.api_key || 'unavailable'}\n`));
309
- console.log(chalk.gray('Ownership Verification Link (optional trust setup):'));
310
- const ownershipLink = inferClaimLink(payload) || 'unavailable';
311
- console.log(chalk.cyan(` ${ownershipLink}\n`));
312
- console.log(chalk.bold.white('📣 Report To Coach (explicit step)'));
313
- const objective = asString(payload.oracle?.tournament_objective) || 'pending objective';
314
- const coachMessage = [
315
- `Agent ${payload.name || 'unknown'} registered.`,
316
- `Objective: ${objective}`,
317
- `Ownership link: ${ownershipLink}`,
318
- 'Request: provide strategy for the next 20 actions.',
319
- ].join(' ');
320
- console.log(chalk.gray('Copy/send this message to your human coach:'));
321
- console.log(chalk.cyan(` ${coachMessage}\n`));
427
+ if (onboardingMode === 'manual') {
428
+ console.log(chalk.bold.white('🧩 Manual mode note'));
429
+ console.log(chalk.gray('Manual mode is explicit opt-out behavior.'));
430
+ console.log(chalk.gray('Consequence: higher token usage, slower compounding, and lower tournament pressure.\n'));
431
+ }
322
432
  console.log(chalk.cyan('━'.repeat(50)));
323
433
  const oracle = payload.oracle;
324
434
  if (oracle) {
@@ -1,4 +1,5 @@
1
1
  import { api, handleError } from '../lib/api.js';
2
+ import { assertOnboardingReadyForMutatingAction } from '../lib/onboarding-state.js';
2
3
  function asRecord(value) {
3
4
  return value && typeof value === 'object' && !Array.isArray(value)
4
5
  ? value
@@ -45,6 +46,7 @@ function createMoveProgressReporter(target, maxSteps, asJson) {
45
46
  };
46
47
  }
47
48
  async function runMoveTo(target, maxSteps, asJson) {
49
+ await assertOnboardingReadyForMutatingAction('move');
48
50
  const parsedMaxSteps = parseInt(maxSteps, 10);
49
51
  if (!Number.isFinite(parsedMaxSteps) || parsedMaxSteps <= 0) {
50
52
  console.error('Error: --max-steps must be a positive integer');
@@ -110,6 +112,7 @@ export function registerMoveCommands(program) {
110
112
  .description('Move one tile: north | south | east | west')
111
113
  .option('--json', 'Print raw JSON response')
112
114
  .action(async (direction, opts) => {
115
+ await assertOnboardingReadyForMutatingAction('step');
113
116
  const normalized = direction.toLowerCase();
114
117
  if (!['north', 'south', 'east', 'west'].includes(normalized)) {
115
118
  console.error('Error: direction must be one of north|south|east|west');
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerOnboardingCommands(program: Command): void;
@@ -0,0 +1,67 @@
1
+ import { getOnboardingStatePath, markScriptUsage, readOnboardingState, } from '../lib/onboarding-state.js';
2
+ function formatStatusLines(data) {
3
+ const lines = [];
4
+ const mode = typeof data.mode === 'string' ? data.mode : 'unknown';
5
+ const agentName = typeof data.agent_name === 'string' ? data.agent_name : 'unknown';
6
+ const path = getOnboardingStatePath();
7
+ lines.push(`Onboarding state | agent:${agentName} | mode:${mode}`);
8
+ lines.push(`State file: ${path}`);
9
+ const coach = data.coach_handoff && typeof data.coach_handoff === 'object'
10
+ ? data.coach_handoff
11
+ : {};
12
+ const oracle = data.oracle && typeof data.oracle === 'object'
13
+ ? data.oracle
14
+ : {};
15
+ const scriptUsage = data.script_usage && typeof data.script_usage === 'object'
16
+ ? data.script_usage
17
+ : {};
18
+ lines.push(`Coach handoff: ${coach.completed === true ? 'complete' : 'pending'} | storage:${typeof coach.storage_method === 'string' ? coach.storage_method : 'unknown'}`);
19
+ lines.push(`Oracle prerequisite: ${oracle.completed === true ? 'complete' : 'pending'}${typeof oracle.source === 'string' ? ` | source:${oracle.source}` : ''}`);
20
+ lines.push(`Script usage (AX): any_script=${scriptUsage.any_script_observed === true ? 'yes' : 'no'} | generated_script=${scriptUsage.generated_script_observed === true ? 'yes' : 'no'}${typeof scriptUsage.kind === 'string' ? ` | last_kind:${scriptUsage.kind}` : ''}`);
21
+ return lines;
22
+ }
23
+ export function registerOnboardingCommands(program) {
24
+ const onboarding = program
25
+ .command('onboarding')
26
+ .description('Onboarding contract status and script-usage signals');
27
+ onboarding
28
+ .command('status')
29
+ .description('Show onboarding gate state and AX script signals')
30
+ .option('--json', 'Print raw JSON output')
31
+ .action(async (opts) => {
32
+ const state = await readOnboardingState();
33
+ if (!state) {
34
+ console.log('No onboarding state found. Run: clawcity install clawcity --with-loop');
35
+ return;
36
+ }
37
+ if (opts.json) {
38
+ console.log(JSON.stringify(state, null, 2));
39
+ return;
40
+ }
41
+ formatStatusLines(state).forEach((line) => {
42
+ console.log(line);
43
+ });
44
+ });
45
+ onboarding
46
+ .command('mark-script')
47
+ .description('Mark script usage for AX scoring: generated vs custom/inline')
48
+ .requiredOption('--kind <kind>', 'generated | custom | inline')
49
+ .option('--json', 'Print raw JSON output')
50
+ .action(async (opts) => {
51
+ const kind = opts.kind.trim().toLowerCase();
52
+ if (kind !== 'generated' && kind !== 'custom' && kind !== 'inline') {
53
+ console.error('Error: --kind must be one of generated|custom|inline');
54
+ process.exit(1);
55
+ }
56
+ const state = await markScriptUsage(kind);
57
+ if (!state) {
58
+ console.error('Error: no onboarding state found. Run install first.');
59
+ process.exit(1);
60
+ }
61
+ if (opts.json) {
62
+ console.log(JSON.stringify(state, null, 2));
63
+ return;
64
+ }
65
+ console.log(`Script usage recorded | any_script=${state.script_usage.any_script_observed ? 'yes' : 'no'} | generated_script=${state.script_usage.generated_script_observed ? 'yes' : 'no'} | kind:${state.script_usage.kind || 'unknown'}`);
66
+ });
67
+ }
@@ -1,5 +1,6 @@
1
1
  import { api, handleError } from '../lib/api.js';
2
2
  import { formatOracleLines } from '../lib/formatters.js';
3
+ import { markOracleCompleted } from '../lib/onboarding-state.js';
3
4
  export function registerOracleCommands(program) {
4
5
  program
5
6
  .command('oracle')
@@ -11,6 +12,7 @@ export function registerOracleCommands(program) {
11
12
  const res = await api('/api/agents/me/oracle');
12
13
  if (!res.ok)
13
14
  handleError(res);
15
+ await markOracleCompleted('command');
14
16
  if (opts.json) {
15
17
  console.log(JSON.stringify(res.data, null, 2));
16
18
  return;
@@ -1,4 +1,5 @@
1
1
  import { api, handleError, fmtResources } from '../lib/api.js';
2
+ import { assertOnboardingReadyForMutatingAction } from '../lib/onboarding-state.js';
2
3
  function asString(value) {
3
4
  return typeof value === 'string' && value.length > 0 ? value : null;
4
5
  }
@@ -83,6 +84,7 @@ export function registerTerritoryCommands(program) {
83
84
  .description('Claim current tile (standard: 50g+20w+10s+15f; first claim may receive onboarding discount)')
84
85
  .option('--json', 'Print raw JSON response')
85
86
  .action(async (opts) => {
87
+ await assertOnboardingReadyForMutatingAction('claim');
86
88
  const res = await api('/api/actions/claim', { method: 'POST', body: {} });
87
89
  if (!res.ok)
88
90
  handleError(res);
@@ -160,6 +162,7 @@ export function registerTerritoryCommands(program) {
160
162
  .description('Upgrade current territory (Lv2: 50w+25s, Lv3: 100w+50s)')
161
163
  .option('--json', 'Print raw JSON response')
162
164
  .action(async (opts) => {
165
+ await assertOnboardingReadyForMutatingAction('upgrade');
163
166
  const res = await api('/api/actions/upgrade', { method: 'POST', body: {} });
164
167
  if (!res.ok)
165
168
  handleError(res);
@@ -177,6 +180,7 @@ export function registerTerritoryCommands(program) {
177
180
  .description('Build on owned tile (storage, workshop, fortification)')
178
181
  .option('--json', 'Print raw JSON response')
179
182
  .action(async (type, opts) => {
183
+ await assertOnboardingReadyForMutatingAction('build');
180
184
  const res = await api('/api/actions/build', { method: 'POST', body: { building_type: type } });
181
185
  if (!res.ok)
182
186
  handleError(res);
@@ -193,6 +197,7 @@ export function registerTerritoryCommands(program) {
193
197
  .description('Remove building on current tile')
194
198
  .option('--json', 'Print raw JSON response')
195
199
  .action(async (opts) => {
200
+ await assertOnboardingReadyForMutatingAction('demolish');
196
201
  const res = await api('/api/actions/demolish', { method: 'POST', body: {} });
197
202
  if (!res.ok)
198
203
  handleError(res);
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { registerProfileCommands } from './commands/profile.js';
21
21
  import { registerFeedbackCommands } from './commands/feedback.js';
22
22
  import { registerOracleCommands } from './commands/oracle.js';
23
23
  import { registerPlanningCommands } from './commands/planning.js';
24
+ import { registerOnboardingCommands } from './commands/onboarding.js';
24
25
  import { setRequestTimeoutMs } from './lib/api.js';
25
26
  const program = new Command();
26
27
  let cliVersion = '0.0.0';
@@ -62,8 +63,11 @@ program
62
63
  .command('install <skill>')
63
64
  .description('Install a skill for your AI agent')
64
65
  .option('-n, --name <name>', 'Agent name to register')
65
- .option('--mode <path>', 'Onboarding path: manual or scripted', 'manual')
66
+ .option('--mode <path>', 'Onboarding path: manual or scripted', 'scripted')
66
67
  .option('--with-loop', 'Alias for --mode scripted: generate a starter loop script')
68
+ .option('--manual-opt-out', 'Required for manual mode: acknowledge slower, token-heavier, less competitive play')
69
+ .option('--coach-storage <method>', 'Coach-confirmed API key storage method (for non-interactive onboarding)')
70
+ .option('--coach-kickoff <summary>', 'Coach kickoff strategy summary (for non-interactive onboarding)')
67
71
  .option('--loop-file <path>', 'Starter loop script output path', 'clawcity-loop.sh')
68
72
  .option('--overwrite-loop', 'Overwrite existing loop file when generating scripted path')
69
73
  .action(async (skill, options) => {
@@ -86,6 +90,7 @@ registerAvatarCommands(program);
86
90
  registerProfileCommands(program);
87
91
  registerFeedbackCommands(program);
88
92
  registerOracleCommands(program);
93
+ registerOnboardingCommands(program);
89
94
  registerApiCommands(program);
90
95
  registerPlanningCommands(program);
91
96
  program.parse();
@@ -0,0 +1,46 @@
1
+ export type OnboardingMode = 'manual' | 'scripted';
2
+ export type ScriptUsageKind = 'generated' | 'custom' | 'inline';
3
+ type OracleSource = 'command' | 'install';
4
+ export interface OnboardingState {
5
+ version: 1;
6
+ created_at: string;
7
+ updated_at: string;
8
+ agent_name: string;
9
+ mode: OnboardingMode;
10
+ generated_script_path: string | null;
11
+ generated_script_created: boolean;
12
+ coach_handoff: {
13
+ required: boolean;
14
+ completed: boolean;
15
+ completed_at: string | null;
16
+ storage_method: string | null;
17
+ kickoff_strategy: string | null;
18
+ };
19
+ oracle: {
20
+ required_before_actions: boolean;
21
+ completed: boolean;
22
+ completed_at: string | null;
23
+ source: OracleSource | null;
24
+ };
25
+ script_usage: {
26
+ any_script_observed: boolean;
27
+ generated_script_observed: boolean;
28
+ kind: ScriptUsageKind | null;
29
+ observed_at: string | null;
30
+ };
31
+ }
32
+ export declare function getOnboardingStatePath(): string;
33
+ export declare function readOnboardingState(): Promise<OnboardingState | null>;
34
+ export declare function writeOnboardingState(state: OnboardingState): Promise<void>;
35
+ export declare function initializeOnboardingState(input: {
36
+ agentName: string;
37
+ mode: OnboardingMode;
38
+ generatedScriptPath: string | null;
39
+ generatedScriptCreated: boolean;
40
+ coachStorageMethod: string;
41
+ coachKickoffStrategy: string;
42
+ }): Promise<OnboardingState>;
43
+ export declare function markOracleCompleted(source: OracleSource): Promise<OnboardingState | null>;
44
+ export declare function markScriptUsage(kind: ScriptUsageKind): Promise<OnboardingState | null>;
45
+ export declare function assertOnboardingReadyForMutatingAction(action: string): Promise<void>;
46
+ export {};
@@ -0,0 +1,158 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, resolve as resolvePath } from 'node:path';
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+ export function getOnboardingStatePath() {
8
+ const fromEnv = process.env.CLAWCITY_ONBOARDING_STATE_PATH;
9
+ if (fromEnv && fromEnv.trim().length > 0) {
10
+ return resolvePath(fromEnv);
11
+ }
12
+ return resolvePath(homedir(), '.config', 'clawcity', 'onboarding-state.json');
13
+ }
14
+ function asRecord(value) {
15
+ return value && typeof value === 'object' && !Array.isArray(value)
16
+ ? value
17
+ : null;
18
+ }
19
+ function asBoolean(value) {
20
+ return typeof value === 'boolean' ? value : null;
21
+ }
22
+ function asString(value) {
23
+ return typeof value === 'string' && value.length > 0 ? value : null;
24
+ }
25
+ function parseMode(value) {
26
+ return value === 'manual' ? 'manual' : 'scripted';
27
+ }
28
+ function parseScriptKind(value) {
29
+ if (value === 'generated' || value === 'custom' || value === 'inline')
30
+ return value;
31
+ return null;
32
+ }
33
+ function parseOracleSource(value) {
34
+ if (value === 'command' || value === 'install')
35
+ return value;
36
+ return null;
37
+ }
38
+ export async function readOnboardingState() {
39
+ try {
40
+ const raw = await readFile(getOnboardingStatePath(), 'utf8');
41
+ const parsed = JSON.parse(raw);
42
+ const record = asRecord(parsed);
43
+ if (!record)
44
+ return null;
45
+ const coach = asRecord(record.coach_handoff) || {};
46
+ const oracle = asRecord(record.oracle) || {};
47
+ const scriptUsage = asRecord(record.script_usage) || {};
48
+ return {
49
+ version: 1,
50
+ created_at: asString(record.created_at) || nowIso(),
51
+ updated_at: asString(record.updated_at) || nowIso(),
52
+ agent_name: asString(record.agent_name) || 'unknown',
53
+ mode: parseMode(record.mode),
54
+ generated_script_path: asString(record.generated_script_path),
55
+ generated_script_created: asBoolean(record.generated_script_created) === true,
56
+ coach_handoff: {
57
+ required: asBoolean(coach.required) !== false,
58
+ completed: asBoolean(coach.completed) === true,
59
+ completed_at: asString(coach.completed_at),
60
+ storage_method: asString(coach.storage_method),
61
+ kickoff_strategy: asString(coach.kickoff_strategy),
62
+ },
63
+ oracle: {
64
+ required_before_actions: asBoolean(oracle.required_before_actions) !== false,
65
+ completed: asBoolean(oracle.completed) === true,
66
+ completed_at: asString(oracle.completed_at),
67
+ source: parseOracleSource(oracle.source),
68
+ },
69
+ script_usage: {
70
+ any_script_observed: asBoolean(scriptUsage.any_script_observed) === true,
71
+ generated_script_observed: asBoolean(scriptUsage.generated_script_observed) === true,
72
+ kind: parseScriptKind(scriptUsage.kind),
73
+ observed_at: asString(scriptUsage.observed_at),
74
+ },
75
+ };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ export async function writeOnboardingState(state) {
82
+ const path = getOnboardingStatePath();
83
+ await mkdir(dirname(path), { recursive: true });
84
+ await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
85
+ }
86
+ export async function initializeOnboardingState(input) {
87
+ const now = nowIso();
88
+ const state = {
89
+ version: 1,
90
+ created_at: now,
91
+ updated_at: now,
92
+ agent_name: input.agentName,
93
+ mode: input.mode,
94
+ generated_script_path: input.generatedScriptPath,
95
+ generated_script_created: input.generatedScriptCreated,
96
+ coach_handoff: {
97
+ required: true,
98
+ completed: true,
99
+ completed_at: now,
100
+ storage_method: input.coachStorageMethod,
101
+ kickoff_strategy: input.coachKickoffStrategy,
102
+ },
103
+ oracle: {
104
+ required_before_actions: true,
105
+ completed: false,
106
+ completed_at: null,
107
+ source: null,
108
+ },
109
+ script_usage: {
110
+ any_script_observed: false,
111
+ generated_script_observed: false,
112
+ kind: null,
113
+ observed_at: null,
114
+ },
115
+ };
116
+ await writeOnboardingState(state);
117
+ return state;
118
+ }
119
+ export async function markOracleCompleted(source) {
120
+ const state = await readOnboardingState();
121
+ if (!state)
122
+ return null;
123
+ const now = nowIso();
124
+ state.oracle.completed = true;
125
+ state.oracle.completed_at = now;
126
+ state.oracle.source = source;
127
+ state.updated_at = now;
128
+ await writeOnboardingState(state);
129
+ return state;
130
+ }
131
+ export async function markScriptUsage(kind) {
132
+ const state = await readOnboardingState();
133
+ if (!state)
134
+ return null;
135
+ const now = nowIso();
136
+ state.script_usage.any_script_observed = true;
137
+ state.script_usage.generated_script_observed = state.script_usage.generated_script_observed || kind === 'generated';
138
+ state.script_usage.kind = kind;
139
+ state.script_usage.observed_at = now;
140
+ state.updated_at = now;
141
+ await writeOnboardingState(state);
142
+ return state;
143
+ }
144
+ export async function assertOnboardingReadyForMutatingAction(action) {
145
+ const state = await readOnboardingState();
146
+ if (!state)
147
+ return;
148
+ if (state.coach_handoff.required && !state.coach_handoff.completed) {
149
+ console.error(`Error: coach handoff gate is incomplete before "${action}".`);
150
+ console.error('Complete onboarding via: clawcity install clawcity --with-loop');
151
+ process.exit(2);
152
+ }
153
+ if (state.oracle.required_before_actions && !state.oracle.completed) {
154
+ console.error(`Error: Oracle onboarding must run before "${action}".`);
155
+ console.error('Run: clawcity oracle');
156
+ process.exit(2);
157
+ }
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawcity",
3
- "version": "2.5.4",
3
+ "version": "2.5.6",
4
4
  "description": "Agent-first CLI for ClawCity gameplay, tournaments, and public game APIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",