docs: clean up Paperclip references and update Aider+Pi handoff (Sam & Claude)

Remove Paperclip name from descriptive comments in setup/agent-cli-check.ts
and setup/onboarding.ts. Fix stale SQLite reference in
doc/CONTROLPLANE-AIDER-PI.md. Answer open questions and check off
completed tasks in the Aider+Pi handoff doc.

---
Build: pass | Tests: not run (Linux)
This commit is contained in:
Clawdie AI 2026-04-11 20:51:46 +02:00
parent 405dd6f9d9
commit 05fc2c30d7
4 changed files with 129 additions and 93 deletions

View file

@ -6,32 +6,35 @@
## Deletion Criteria
- [ ] Docs updated to remove Paperclip controlplane UI references
- [ ] New Aider+Pi harness docs added (entry-point and workflow)
- [x] Docs updated to remove Paperclip controlplane UI references
- [x] New Aider+Pi harness docs added (entry-point and workflow)
- [ ] README updated to reflect Aider+Pi as the controlplane driver
## Tasks
- [ ] Inventory Paperclip/controlplane UI references and propose removals or updates
- Likely files: `README.md`, `html/docs-clawdie-si/`, `doc/CONTROLPLANE-*.md`,
`setup/install.ts`, `setup/verify.ts`, `scripts/*`
- [ ] Draft new harness doc describing Aider+Pi workflow and multi-agent usage
- Suggested location: `doc/CONTROLPLANE-AIDER-PI.md`
- [x] Inventory Paperclip/controlplane UI references and propose removals or updates
- 119 matches in `.md` — all in archived docs (`DASHBOARD-*.md`, `AGENTIC-HARNESS-PIVOT.md`)
- 2 matches in `.ts` — descriptive comments only (cleaned up)
- 0 functional code paths assuming Paperclip UI
- [x] Draft new harness doc describing Aider+Pi workflow and multi-agent usage
- `doc/CONTROLPLANE-AIDER-PI.md`
- [ ] Update install docs to position Aider+Pi as the default controlplane path
- Likely files: `README.md`, `html/docs-clawdie-si/docs/install.html`
- [ ] Flag any code paths that assume a UI build (Paperclip) and list them
- Provide a short list for FreeBSD agent to act on
- Deferred: README.md, `html/docs-clawdie-si/docs/install.html`
- [x] Flag any code paths that assume a UI build (Paperclip) and list them
- None found. All Paperclip code paths were already removed by the agentic harness pivot.
## Results (fill in when done)
- Build: pass/fail
- Build: pass
- Tests: N/A (Linux agent does not run vitest here)
- Bugs found: none
- Bugs found: stale SQLite reference in `doc/CONTROLPLANE-AIDER-PI.md` (fixed)
## Open Questions
- Is there any remaining Paperclip UI code path that should be preserved as an optional plug-in?
- Do we want a separate nginx route for agent-generated dashboards, or reuse an existing controlplane path?
- ~~Is there any remaining Paperclip UI code path that should be preserved as an optional plug-in?~~
**No.** Zero functional references remain. Only archived docs and 2 descriptive comments (cleaned up).
- ~~Do we want a separate nginx route for agent-generated dashboards, or reuse an existing controlplane path?~~
**Reuse existing path.** `CONTROLPLANE_DASHBOARD_DIR` defaults to `/usr/local/www/clawdie/controlplane`. nginx already serves it. No new route needed.
## Delete After

View file

@ -17,11 +17,11 @@ The UI becomes agent-generated artifacts served by nginx (no long-lived UI app).
## Architecture (High Level)
1) Operator interacts with Pi in tmux.
2) Pi uses Aider to spawn multi-agent work when needed.
3) Agents write status + dashboards to a known output directory.
4) nginx serves the generated dashboard artifacts.
5) Controlplane API remains the authoritative source of state.
1. Operator interacts with Pi in tmux.
2. Pi uses Aider to spawn multi-agent work when needed.
3. Agents write status + dashboards to a known output directory.
4. nginx serves the generated dashboard artifacts.
5. Controlplane API remains the authoritative source of state.
## Dashboard Output (Agent-Generated UI)
@ -43,8 +43,8 @@ nginx serves as a static site. This replaces Paperclip as the controlplane UI.
## Storage
- Long-term memory remains in PostgreSQL (Data Service (db jail)).
- SQLite remains for any local-only memory features explicitly documented in
`MEMORY.md` (do not migrate without a dedicated design change).
- All data lives in PostgreSQL (three databases: skills, memory, ops). No SQLite
remains in the runtime or setup paths.
## What Changes (Summary)
@ -54,7 +54,6 @@ nginx serves as a static site. This replaces Paperclip as the controlplane UI.
## Next Steps
1) Update install and README docs to state Aider+Pi as the primary harness.
2) Remove Paperclip-specific build steps from setup/verify.
3) Add a controlplane doc page describing the dashboard output contract.
1. Update install and README docs to state Aider+Pi as the primary harness.
2. Remove Paperclip-specific build steps from setup/verify.
3. Add a controlplane doc page describing the dashboard output contract.

View file

@ -3,7 +3,7 @@
*
* Clawdie's runtime drives one of several local agent CLIs (claude, codex,
* gemini, pi). At least one must be resolvable on PATH for onboarding to
* succeed. This mirrors Paperclip's per-adapter `ensureCommandResolvable`
* succeed. This mirrors the standard per-adapter command-resolvable
* pattern, but as a single fail-fast check at the top of `setup onboard`.
*
* No "hello probe" yet `command -v` is enough; all four were validated

View file

@ -11,15 +11,18 @@ import {
normalizeTimeZone,
toSystemLocale,
} from '../src/locale-profile.js';
import {
normalizePiTuiProfile,
PI_TUI_PROFILES,
} from '../src/pi-profile.js';
import { normalizePiTuiProfile, PI_TUI_PROFILES } from '../src/pi-profile.js';
import { resolveAgentIdentity } from '../src/agent-identity.js';
import type { AgentGender } from '../src/config.js';
import { getStripeKeyMode, isValidStripeTestKey } from '../src/stripe-config.js';
import {
getStripeKeyMode,
isValidStripeTestKey,
} from '../src/stripe-config.js';
import { commandExists, getPlatform } from './platform.js';
import { requireAtLeastOneAgentCli, NoAgentCliError } from './agent-cli-check.js';
import {
requireAtLeastOneAgentCli,
NoAgentCliError,
} from './agent-cli-check.js';
import {
detectProfile,
ensureEnvFile,
@ -37,10 +40,7 @@ import {
prioritizeTimezones,
type TimezoneOption,
} from './freebsd-timezones.js';
import {
ensureScreenshotSecrets,
ensureSplitBrainSecrets,
} from './secrets.js';
import { ensureScreenshotSecrets, ensureSplitBrainSecrets } from './secrets.js';
import { emitStatus } from './status.js';
interface OnboardingArgs {
@ -197,10 +197,14 @@ function defaultPublicDomain(agentName: string): string {
function detectOriginRemote(projectRoot: string): string {
try {
return execFileSync('git', ['-C', projectRoot, 'config', '--get', 'remote.origin.url'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return execFileSync(
'git',
['-C', projectRoot, 'config', '--get', 'remote.origin.url'],
{
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
},
).trim();
} catch {
return 'https://codeberg.org/Clawdie/Clawdie-AI.git';
}
@ -248,14 +252,25 @@ function writeCodeHostingDefaults(envFile: string, projectRoot: string): void {
}
}
function writeIdentity(envFile: string, agentName: string, assistantName: string): void {
function writeIdentity(
envFile: string,
agentName: string,
assistantName: string,
): void {
writeEnvLine(envFile, 'AGENT_NAME', agentName);
writeEnvLine(envFile, 'ASSISTANT_NAME', quoteEnvValue(assistantName));
const content = fs.readFileSync(envFile, 'utf-8');
const currentInternalDomain = extractEnvValue(content, 'AGENT_INTERNAL_DOMAIN');
const currentInternalDomain = extractEnvValue(
content,
'AGENT_INTERNAL_DOMAIN',
);
if (!currentInternalDomain || currentInternalDomain.endsWith('.local')) {
writeEnvLine(envFile, 'AGENT_INTERNAL_DOMAIN', defaultInternalDomain(agentName));
writeEnvLine(
envFile,
'AGENT_INTERNAL_DOMAIN',
defaultInternalDomain(agentName),
);
}
const withInternalDomain = fs.readFileSync(envFile, 'utf-8');
@ -294,7 +309,8 @@ function applyHostLocale(systemLocale: string): boolean {
// rewrite the locale tag with .UTF-8 so ~/.login_conf never locks the
// user into a legacy encoding like ISO8859-2.
const dotIdx = systemLocale.indexOf('.');
const baseLocale = dotIdx !== -1 ? systemLocale.slice(0, dotIdx) : systemLocale;
const baseLocale =
dotIdx !== -1 ? systemLocale.slice(0, dotIdx) : systemLocale;
const normalizedLocale = `${baseLocale}.UTF-8`;
const content = `me:\\\n\t:charset=UTF-8:\\\n\t:lang=${normalizedLocale}:\n`;
const loginConfPath = path.join(os.homedir(), '.login_conf');
@ -365,10 +381,7 @@ function showTimezoneMenu(
text: string,
options: TimezoneOption[],
): TimezoneOption {
const rawItems = options.flatMap((option) => [
option.timezone,
option.label,
]);
const rawItems = options.flatMap((option) => [option.timezone, option.label]);
const selected = runBsddialog([
'--title',
title,
@ -387,15 +400,7 @@ function showTimezoneMenu(
}
function showInputBox(title: string, text: string, value: string): string {
return runBsddialog([
'--title',
title,
'--inputbox',
text,
'0',
'0',
value,
]);
return runBsddialog(['--title', title, '--inputbox', text, '0', '0', value]);
}
function showPasswordBox(title: string, text: string, value = ''): string {
@ -411,23 +416,14 @@ function showPasswordBox(title: string, text: string, value = ''): string {
}
function showMessageBox(title: string, text: string): void {
runBsddialog([
'--title',
title,
'--msgbox',
text,
'0',
'0',
]);
runBsddialog(['--title', title, '--msgbox', text, '0', '0']);
}
function showYesNo(title: string, text: string): boolean {
try {
execFileSync(
'bsddialog',
['--title', title, '--yesno', text, '0', '0'],
{ stdio: 'inherit' },
);
execFileSync('bsddialog', ['--title', title, '--yesno', text, '0', '0'], {
stdio: 'inherit',
});
return true;
} catch (error) {
const status =
@ -527,16 +523,23 @@ function showGenderMenu(
locale: string,
): AgentGender {
const items = [
'f', `ženski — ${genderPreview(assistantName, 'f', locale)}`,
'm', `moški — ${genderPreview(assistantName, 'm', locale)}`,
'n', 'nevtralno — neutral / English',
'f',
`ženski — ${genderPreview(assistantName, 'f', locale)}`,
'm',
`moški — ${genderPreview(assistantName, 'm', locale)}`,
'n',
'nevtralno — neutral / English',
];
const selected = runBsddialog([
'--title', 'Spol asistenta / Assistant Gender',
'--default-item', suggestedGender,
'--title',
'Spol asistenta / Assistant Gender',
'--default-item',
suggestedGender,
'--menu',
'Izberite spol. Vpliva na slovnično obliko in samopredstavitev asistenta.\nSelect gender. Affects grammar and self-description.',
'0', '0', '3',
'0',
'0',
'3',
...items,
]);
return normalizeGender(selected);
@ -553,7 +556,7 @@ export async function run(args: string[]): Promise<void> {
const opts = parseArgs(args);
// Fail-fast: at least one agent CLI must resolve on PATH. Mirrors
// Paperclip's per-adapter ensureCommandResolvable, but as a single gate.
// the standard per-adapter ensureCommandResolvable pattern, but as a single gate.
try {
const clis = requireAtLeastOneAgentCli();
const present = clis.filter((c) => c.present).map((c) => c.name);
@ -561,7 +564,10 @@ export async function run(args: string[]): Promise<void> {
logger.info({ present, missing }, 'Agent CLIs detected');
} catch (err) {
if (err instanceof NoAgentCliError) {
emitStatus('SETUP_ONBOARDING', { STATUS: 'failed', ERROR: 'no_agent_cli' });
emitStatus('SETUP_ONBOARDING', {
STATUS: 'failed',
ERROR: 'no_agent_cli',
});
logger.error(err.message);
}
throw err;
@ -571,7 +577,8 @@ export async function run(args: string[]): Promise<void> {
const envFile = ensureEnvFile(projectRoot);
const envContent = fs.readFileSync(envFile, 'utf-8');
const detected = detectProfile(envContent);
const existingAgentName = extractEnvValue(envContent, 'AGENT_NAME') || 'clawdie';
const existingAgentName =
extractEnvValue(envContent, 'AGENT_NAME') || 'clawdie';
const existingAssistantName =
extractEnvValue(envContent, 'ASSISTANT_NAME') || 'Clawdie';
const existingCustomAgentOverride = hasCustomAgentNameOverride(
@ -580,16 +587,21 @@ export async function run(args: string[]): Promise<void> {
);
let displayLocale = normalizeLocaleTag(
opts.locale || extractEnvValue(envContent, 'DISPLAY_LOCALE') || detected.displayLocale,
opts.locale ||
extractEnvValue(envContent, 'DISPLAY_LOCALE') ||
detected.displayLocale,
);
let systemLocale =
extractEnvValue(envContent, 'SYSTEM_LOCALE') || detected.systemLocale;
let timeZone = normalizeTimeZone(
opts.timezone || extractEnvValue(envContent, 'TZ') || detected.timeZone,
);
let assistantName = (opts.assistantName || existingAssistantName).trim() || 'Clawdie';
let assistantName =
(opts.assistantName || existingAssistantName).trim() || 'Clawdie';
let stripeSecretKey = (
opts.stripeSecretKey || extractEnvValue(envContent, 'STRIPE_SECRET_KEY') || ''
opts.stripeSecretKey ||
extractEnvValue(envContent, 'STRIPE_SECRET_KEY') ||
''
).trim();
let stripeMode = getStripeKeyMode(stripeSecretKey);
let agentName = opts.agentName
@ -637,7 +649,10 @@ export async function run(args: string[]): Promise<void> {
displayLocale = localeOption.displayLocale;
systemLocale = localeOption.systemLocale;
const timezones = prioritizeTimezones(getTimezoneOptions(), timeZone || 'Europe/Ljubljana');
const timezones = prioritizeTimezones(
getTimezoneOptions(),
timeZone || 'Europe/Ljubljana',
);
const timezoneOption = showTimezoneMenu(
'Clawdie Onboarding',
'Select your timezone for system date/time and scheduling.',
@ -712,7 +727,10 @@ export async function run(args: string[]): Promise<void> {
}
}
while (promptForStripeKey || (stripeSecretKey && !isValidStripeTestKey(stripeSecretKey))) {
while (
promptForStripeKey ||
(stripeSecretKey && !isValidStripeTestKey(stripeSecretKey))
) {
promptForStripeKey = false;
stripeSecretKey = showPasswordBox(
'Stripe Test Key',
@ -825,10 +843,12 @@ export async function run(args: string[]): Promise<void> {
const hostLocaleApplied = applyHostLocale(systemLocale);
if (hostLocaleApplied) {
runBsddialog([
'--title', 'Host Locale Set',
'--title',
'Host Locale Set',
'--msgbox',
`Locale set to ${systemLocale} (normalised to UTF-8).\n\nWritten to ~/.login_conf.\n\nOpen a new tmux window or fresh SSH login to activate.`,
'0', '0',
'0',
'0',
]);
}
@ -846,26 +866,38 @@ export async function run(args: string[]): Promise<void> {
displayLocale,
);
console.log('Interactive onboarding: bsddialog not found, using plain TTY prompts.');
console.log(
'Interactive onboarding: bsddialog not found, using plain TTY prompts.',
);
displayLocale = normalizeLocaleTag(
await promptWithDefault(rl, 'Base locale', displayLocale),
displayLocale,
);
systemLocale =
locales.find((option) => option.displayLocale === displayLocale)?.systemLocale ||
toSystemLocale(displayLocale);
locales.find((option) => option.displayLocale === displayLocale)
?.systemLocale || toSystemLocale(displayLocale);
timeZone = normalizeTimeZone(
await promptWithDefault(rl, 'Timezone (IANA format, e.g., Europe/Ljubljana)', timeZone),
await promptWithDefault(
rl,
'Timezone (IANA format, e.g., Europe/Ljubljana)',
timeZone,
),
timeZone,
);
assistantName =
(await promptWithDefault(rl, 'Assistant name', assistantName)).trim() ||
assistantName;
derivedAgentName = deriveAgentName(assistantName);
const genderRaw = await promptWithDefault(rl, 'Gender [m/f/n] (ž=f)', opts.agentGender || agentGender);
const genderRaw = await promptWithDefault(
rl,
'Gender [m/f/n] (ž=f)',
opts.agentGender || agentGender,
);
agentGender = normalizeGender(genderRaw);
console.log(`${genderPreview(assistantName, agentGender, displayLocale)}`);
console.log(
`${genderPreview(assistantName, agentGender, displayLocale)}`,
);
console.log(`PI profiles: ${Object.keys(PI_TUI_PROFILES).join(', ')}`);
piProfile = normalizePiTuiProfile(
@ -897,7 +929,9 @@ export async function run(args: string[]): Promise<void> {
'Stripe is not configured. Add STRIPE_SECRET_KEY later or rerun onboarding with --stripe-secret-key.',
);
} else {
console.log(`Stripe status: ${formatStripeSummary(stripeMode)}. Keeping current value.`);
console.log(
`Stripe status: ${formatStripeSummary(stripeMode)}. Keeping current value.`,
);
}
const confirmed = await promptYesNo(