@pugi/cli 0.1.0-beta.1 → 0.1.0-beta.10
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -40
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/prompts.js +8 -0
- package/dist/core/lsp/client.js +719 -0
- package/dist/core/repl/session.js +26 -1
- package/dist/core/repl/slash-commands.js +33 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/runtime/cli.js +282 -7
- package/dist/runtime/commands/delegate.js +289 -0
- package/dist/runtime/commands/lsp.js +206 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/roster.js +117 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/tools/apply-patch.js +495 -0
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tui/repl-render.js +169 -10
- package/dist/tui/repl.js +15 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +6 -4
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi roster` command - α7.5 Tier 1 instantiation Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* Lists the live Tier 1 personas with display name, role, and routing
|
|
5
|
+
* tag. The CLI walks two sources in order:
|
|
6
|
+
*
|
|
7
|
+
* 1. The local @pugi/personas roster (THE_TEN). Always succeeds; the
|
|
8
|
+
* ten brand-canonical personas are baked into the SDK.
|
|
9
|
+
* 2. The remote `GET /api/pugi/sessions/roster` endpoint when the
|
|
10
|
+
* operator has a valid credential. The remote response carries the
|
|
11
|
+
* server-side dispatch role + dispatchTag for each slug so the
|
|
12
|
+
* operator sees the actual routing decision the dispatcher will
|
|
13
|
+
* apply on a `pugi delegate <slug>` call.
|
|
14
|
+
*
|
|
15
|
+
* The command never fails if the network is unreachable - it falls back
|
|
16
|
+
* to local-only output with a one-line warning. This matches the
|
|
17
|
+
* local-first contract (ADR-0037): the operator can still see who is on
|
|
18
|
+
* the team without an API key.
|
|
19
|
+
*
|
|
20
|
+
* Output:
|
|
21
|
+
* - text default: a 3-column table (slug | name | role).
|
|
22
|
+
* - --json: a structured array of { slug, name, role, totem,
|
|
23
|
+
* dispatchTag } records, used by scripted callers.
|
|
24
|
+
*/
|
|
25
|
+
import { THE_TEN } from '@pugi/personas';
|
|
26
|
+
import { fetchPersonaRoster, } from '@pugi/sdk';
|
|
27
|
+
/**
|
|
28
|
+
* Fallback role + tag table the CLI uses when the runtime is unreachable
|
|
29
|
+
* (no credentials, network error, older runtime without the
|
|
30
|
+
* /sessions/roster endpoint). Mirrors the server-side
|
|
31
|
+
* persona-dispatch.ts PERSONA_REGISTRY so a CLI that ran without
|
|
32
|
+
* credentials still shows the operator the right routing intent.
|
|
33
|
+
*/
|
|
34
|
+
const FALLBACK_ROLE_BY_SLUG = Object.freeze({
|
|
35
|
+
main: { role: 'orchestrator', dispatchTag: 'reason' },
|
|
36
|
+
architect: { role: 'architect', dispatchTag: 'reason' },
|
|
37
|
+
dev: { role: 'coder', dispatchTag: 'codegen' },
|
|
38
|
+
qa: { role: 'verifier', dispatchTag: 'reason' },
|
|
39
|
+
pm: { role: 'release', dispatchTag: 'reason' },
|
|
40
|
+
devops: { role: 'devops', dispatchTag: 'reason' },
|
|
41
|
+
researcher: { role: 'researcher', dispatchTag: 'reason' },
|
|
42
|
+
analyst: { role: 'analyst', dispatchTag: 'summarize' },
|
|
43
|
+
designer: { role: 'design_qa', dispatchTag: 'reason' },
|
|
44
|
+
frontend: { role: 'frontend', dispatchTag: 'codegen' },
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Build the roster rows by merging the local @pugi/personas brand
|
|
48
|
+
* roster with the remote dispatch metadata when a credential is
|
|
49
|
+
* available. Pure function so the runtime CLI command can unit-test it
|
|
50
|
+
* without standing up an Anvil endpoint.
|
|
51
|
+
*/
|
|
52
|
+
export function mergeRoster(brandRoster, remote) {
|
|
53
|
+
const remoteIndex = new Map((remote ?? []).map((entry) => [entry.slug, entry]));
|
|
54
|
+
return brandRoster.map((persona) => {
|
|
55
|
+
const fromRemote = remoteIndex.get(persona.slug);
|
|
56
|
+
const fallback = FALLBACK_ROLE_BY_SLUG[persona.slug] ?? {
|
|
57
|
+
role: persona.role,
|
|
58
|
+
dispatchTag: 'reason',
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
slug: persona.slug,
|
|
62
|
+
name: persona.name,
|
|
63
|
+
totem: persona.animal,
|
|
64
|
+
role: fromRemote?.role ?? fallback.role,
|
|
65
|
+
dispatchTag: fromRemote?.dispatchTag ?? fallback.dispatchTag,
|
|
66
|
+
oneLiner: persona.oneLiner,
|
|
67
|
+
source: fromRemote ? 'remote' : 'local-fallback',
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Render a roster as a plain-text 3-column table the operator reads in
|
|
73
|
+
* the terminal. The column widths grow to fit the longest cell so a
|
|
74
|
+
* future displayName drift does not truncate silently.
|
|
75
|
+
*/
|
|
76
|
+
export function renderRosterTable(rows) {
|
|
77
|
+
if (rows.length === 0)
|
|
78
|
+
return 'Roster is empty.';
|
|
79
|
+
const head = { slug: 'slug', name: 'name', totem: 'totem', role: 'role', dispatchTag: 'tag' };
|
|
80
|
+
const widths = {
|
|
81
|
+
slug: Math.max(head.slug.length, ...rows.map((r) => r.slug.length)),
|
|
82
|
+
name: Math.max(head.name.length, ...rows.map((r) => r.name.length)),
|
|
83
|
+
totem: Math.max(head.totem.length, ...rows.map((r) => r.totem.length)),
|
|
84
|
+
role: Math.max(head.role.length, ...rows.map((r) => r.role.length)),
|
|
85
|
+
dispatchTag: Math.max(head.dispatchTag.length, ...rows.map((r) => r.dispatchTag.length)),
|
|
86
|
+
};
|
|
87
|
+
const pad = (s, width) => s + ' '.repeat(Math.max(0, width - s.length));
|
|
88
|
+
const line = (r) => [pad(r.slug, widths.slug), pad(r.name, widths.name), pad(r.totem, widths.totem), pad(r.role, widths.role), pad(r.dispatchTag, widths.dispatchTag)].join(' ');
|
|
89
|
+
const header = line(head);
|
|
90
|
+
const sep = '-'.repeat(header.length);
|
|
91
|
+
return [header, sep, ...rows.map((r) => line(r))].join('\n');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the roster by walking remote + local sources. The CLI command
|
|
95
|
+
* is a thin wrapper around this function so unit tests can exercise the
|
|
96
|
+
* merge logic without hitting the runtime.
|
|
97
|
+
*/
|
|
98
|
+
export async function resolveRoster(config) {
|
|
99
|
+
if (!config) {
|
|
100
|
+
return {
|
|
101
|
+
rows: mergeRoster(THE_TEN, null),
|
|
102
|
+
warning: 'no credential configured; showing local @pugi/personas roster only',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const result = await fetchPersonaRoster(config);
|
|
106
|
+
if (result.status === 'ok') {
|
|
107
|
+
return { rows: mergeRoster(THE_TEN, result.response.personas), warning: null };
|
|
108
|
+
}
|
|
109
|
+
const reason = result.status === 'endpoint_missing'
|
|
110
|
+
? 'runtime does not expose /api/pugi/sessions/roster (upgrade admin-api to α7.5+)'
|
|
111
|
+
: result.message;
|
|
112
|
+
return {
|
|
113
|
+
rows: mergeRoster(THE_TEN, null),
|
|
114
|
+
warning: `roster fetch failed (${result.status}): ${reason}; showing local roster only`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=roster.js.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi worktree <op>` — α7.7 Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* Manual control over the scratch worktree primitive. Three subcommands:
|
|
5
|
+
*
|
|
6
|
+
* pugi worktree create [branch] # spawns `.pugi/worktrees/<uuid>`
|
|
7
|
+
* pugi worktree promote <path> # applies the worktree's diff back to cwd
|
|
8
|
+
* pugi worktree drop <path> # removes the worktree (idempotent)
|
|
9
|
+
*
|
|
10
|
+
* Output: human-readable by default, structured JSON under --json so
|
|
11
|
+
* scripted callers can chain (`pugi worktree create --json | jq .path`).
|
|
12
|
+
*
|
|
13
|
+
* The same primitives are used by the `pugi build` and
|
|
14
|
+
* `pugi review --consensus` paths internally; this surface is the
|
|
15
|
+
* operator escape hatch for debugging / manual experimentation.
|
|
16
|
+
*
|
|
17
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
18
|
+
*/
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { resolve, sep } from 'node:path';
|
|
21
|
+
import { createWorktree, dropWorktree, promoteWorktree } from '../../core/edits/worktree.js';
|
|
22
|
+
/**
|
|
23
|
+
* R1 fix (2026-05-26, PR #413 r1, P2 #10): operator-facing path
|
|
24
|
+
* validation. The core `promoteWorktree` / `dropWorktree` primitives
|
|
25
|
+
* already gate their inputs, but mirroring the check at the CLI surface
|
|
26
|
+
* gives the operator a clean error message before we even attempt the
|
|
27
|
+
* git invocation (which would otherwise leak `git rev-parse HEAD` stderr).
|
|
28
|
+
*/
|
|
29
|
+
function isUnderScratchRoot(cwd, candidate) {
|
|
30
|
+
const scratchRoot = resolve(cwd, '.pugi', 'worktrees');
|
|
31
|
+
const abs = resolve(cwd, candidate);
|
|
32
|
+
return abs.startsWith(scratchRoot + sep) && abs !== scratchRoot;
|
|
33
|
+
}
|
|
34
|
+
export async function runWorktreeCommand(args, opts) {
|
|
35
|
+
const [op, ...rest] = args;
|
|
36
|
+
if (!op)
|
|
37
|
+
return usage();
|
|
38
|
+
if (op === 'create') {
|
|
39
|
+
const branch = rest[0];
|
|
40
|
+
const result = createWorktree({
|
|
41
|
+
cwd: opts.cwd,
|
|
42
|
+
...(branch ? { branch } : {}),
|
|
43
|
+
});
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
text: opts.json
|
|
48
|
+
? JSON.stringify(result, null, 2)
|
|
49
|
+
: `worktree create failed: ${result.reason}: ${result.detail}`,
|
|
50
|
+
exitCode: 1,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const handle = result.value;
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
text: opts.json
|
|
57
|
+
? JSON.stringify({ path: handle.path, baseSha: handle.baseSha }, null, 2)
|
|
58
|
+
: `worktree created: ${handle.path}\nbase: ${handle.baseSha}`,
|
|
59
|
+
exitCode: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (op === 'promote') {
|
|
63
|
+
const worktreePath = rest[0];
|
|
64
|
+
if (!worktreePath) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
text: 'Usage: pugi worktree promote <path>',
|
|
68
|
+
exitCode: 2,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!isUnderScratchRoot(opts.cwd, worktreePath)) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
text: opts.json
|
|
75
|
+
? JSON.stringify({
|
|
76
|
+
ok: false,
|
|
77
|
+
reason: 'invalid_worktree_path',
|
|
78
|
+
detail: `worktree path must live under <cwd>/.pugi/worktrees/`,
|
|
79
|
+
}, null, 2)
|
|
80
|
+
: `promote failed: invalid_worktree_path: ${worktreePath} is not under .pugi/worktrees/`,
|
|
81
|
+
exitCode: 3,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Resolve the worktree path's base SHA from its own git HEAD so
|
|
85
|
+
// the operator never has to remember it after `worktree create`.
|
|
86
|
+
const abs = resolve(opts.cwd, worktreePath);
|
|
87
|
+
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: abs, encoding: 'utf8' });
|
|
88
|
+
if (head.status !== 0) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
text: `cannot read HEAD of ${abs}: ${head.stderr.trim() || 'git rev-parse failed'}`,
|
|
92
|
+
exitCode: 1,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const baseSha = head.stdout.trim();
|
|
96
|
+
const result = promoteWorktree({
|
|
97
|
+
cwd: opts.cwd,
|
|
98
|
+
worktreePath: abs,
|
|
99
|
+
baseSha,
|
|
100
|
+
...(opts.dryRun ? { dryRun: true } : {}),
|
|
101
|
+
});
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
text: opts.json
|
|
106
|
+
? JSON.stringify(result, null, 2)
|
|
107
|
+
: `promote failed: ${result.reason}: ${result.detail}`,
|
|
108
|
+
exitCode: result.reason === 'protected_file_in_worktree' ? 3 : 1,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const prefix = opts.dryRun ? 'dry-run: would promote' : 'promoted';
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
text: opts.json
|
|
115
|
+
? JSON.stringify({ filesChanged: result.value.filesChanged, dryRun: opts.dryRun ?? false }, null, 2)
|
|
116
|
+
: `${prefix} ${result.value.filesChanged} files from ${abs}`,
|
|
117
|
+
exitCode: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (op === 'drop') {
|
|
121
|
+
const worktreePath = rest[0];
|
|
122
|
+
if (!worktreePath) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
text: 'Usage: pugi worktree drop <path>',
|
|
126
|
+
exitCode: 2,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!isUnderScratchRoot(opts.cwd, worktreePath)) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
text: opts.json
|
|
133
|
+
? JSON.stringify({
|
|
134
|
+
ok: false,
|
|
135
|
+
reason: 'invalid_worktree_path',
|
|
136
|
+
detail: `worktree path must live under <cwd>/.pugi/worktrees/`,
|
|
137
|
+
}, null, 2)
|
|
138
|
+
: `drop failed: invalid_worktree_path: ${worktreePath} is not under .pugi/worktrees/`,
|
|
139
|
+
exitCode: 3,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const abs = resolve(opts.cwd, worktreePath);
|
|
143
|
+
const result = dropWorktree(abs, opts.cwd);
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
text: opts.json
|
|
148
|
+
? JSON.stringify(result, null, 2)
|
|
149
|
+
: `drop failed: ${result.reason}: ${result.detail}`,
|
|
150
|
+
exitCode: result.reason === 'invalid_worktree_path' ? 3 : 1,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
text: opts.json
|
|
156
|
+
? JSON.stringify({ dropped: abs }, null, 2)
|
|
157
|
+
: `worktree dropped: ${abs}`,
|
|
158
|
+
exitCode: 0,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
text: `unknown worktree operation: ${op}. Supported: create, promote, drop`,
|
|
164
|
+
exitCode: 2,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function usage() {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
text: 'Usage: pugi worktree <op>\n' +
|
|
171
|
+
' pugi worktree create [branch]\n' +
|
|
172
|
+
' pugi worktree promote <path>\n' +
|
|
173
|
+
' pugi worktree drop <path>',
|
|
174
|
+
exitCode: 2,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=worktree.js.map
|