@ulysses-ai/create-workspace 0.18.0-beta.0 → 0.20.0-beta.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/lib/init.mjs +4 -1
- package/lib/payload.mjs +18 -1
- package/lib/payload.test.mjs +55 -0
- package/lib/scaffold.mjs +23 -6
- package/lib/scaffold.test.mjs +59 -0
- package/lib/upgrade.mjs +20 -0
- package/lib/upgrade.test.mjs +119 -0
- package/package.json +3 -3
- package/template/CLAUDE.md.tmpl +3 -0
- package/template/{.claude → _claude}/lib/freshness.mjs +20 -7
- package/template/_claude/lib/registry-check.mjs +172 -0
- package/template/{.claude → _claude}/rules/forge-operations.md +6 -0
- package/template/{.claude → _claude}/rules/memory-guidance.md +4 -0
- package/template/{.claude → _claude}/rules/task-list-mirroring.md +6 -0
- package/template/{.claude → _claude}/scripts/build-workspace-context.mjs +25 -14
- package/template/{.claude → _claude}/scripts/chat-record.mjs +62 -21
- package/template/_claude/scripts/classify-update.mjs +117 -0
- package/template/{.claude → _claude}/scripts/cleanup-work-session.mjs +4 -2
- package/template/{.claude → _claude}/scripts/context-footprint.mjs +139 -30
- package/template/{.claude → _claude}/scripts/forges/github.mjs +2 -1
- package/template/{.claude → _claude}/scripts/forges/interface.mjs +5 -4
- package/template/_claude/scripts/merge-mode.mjs +61 -0
- package/template/{.claude → _claude}/scripts/migrate-canonical-priority.mjs +7 -1
- package/template/_claude/scripts/migrate-claude-md-freshness-include.mjs +55 -0
- package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +19 -8
- package/template/{.claude → _claude}/scripts/migrate-sessions.mjs +444 -121
- package/template/_claude/scripts/task-pr.mjs +555 -0
- package/template/{.claude → _claude}/scripts/task-worktree.mjs +26 -16
- package/template/{.claude → _claude}/scripts/trackers/github-issues.mjs +11 -0
- package/template/{.claude → _claude}/scripts/trackers/interface.mjs +8 -0
- package/template/{.claude → _claude}/skills/braindump/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/complete-work/SKILL.md +25 -77
- package/template/{.claude → _claude}/skills/context-placement/SKILL.md +8 -5
- package/template/{.claude → _claude}/skills/goal-driven-work/SKILL.md +1 -1
- package/template/{.claude → _claude}/skills/handoff/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/maintenance/SKILL.md +49 -17
- package/template/{.claude → _claude}/skills/migrate-sessions/SKILL.md +17 -5
- package/template/{.claude → _claude}/skills/release/SKILL.md +6 -2
- package/template/{.claude → _claude}/skills/start-work/SKILL.md +5 -5
- package/template/{.claude → _claude}/skills/workspace-init/SKILL.md +20 -14
- package/template/_claude/skills/workspace-update/SKILL.md +177 -0
- package/template/_gitignore +3 -0
- package/template/workspace.json.tmpl +1 -1
- package/template/.claude/lib/registry-check.mjs +0 -106
- package/template/.claude/scripts/migrate-claude-md-freshness-include.mjs +0 -30
- package/template/.claude/skills/workspace-update/SKILL.md +0 -134
- /package/template/{.claude → _claude}/agents/aside-researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/implementer.md +0 -0
- /package/template/{.claude → _claude}/agents/researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/reviewer.md +0 -0
- /package/template/{.claude → _claude}/hooks/_utils.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/bash-output-advisory.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/post-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/pre-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/repo-write-detection.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-end.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/subagent-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/version-freshness-check.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/workspace-update-check.mjs +0 -0
- /package/template/{.claude → _claude}/lib/require-node.mjs +0 -0
- /package/template/{.claude → _claude}/lib/session-frontmatter.mjs +0 -0
- /package/template/{.claude → _claude}/recipes/migrate-from-notion.md +0 -0
- /package/template/{.claude → _claude}/rules/agent-rules.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/cloud-infrastructure.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/coherent-revisions.md +0 -0
- /package/template/{.claude → _claude}/rules/config-review.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/documentation.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/git-conventions.md +0 -0
- /package/template/{.claude → _claude}/rules/goal-driven-work.md +0 -0
- /package/template/{.claude → _claude}/rules/honest-pushback.md +0 -0
- /package/template/{.claude → _claude}/rules/local-dev-environment.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/product-integrity.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/scope-guard.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/superpowers-workflow.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/token-economics.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/work-item-tracking.md +0 -0
- /package/template/{.claude → _claude}/rules/workspace-structure.md +0 -0
- /package/template/{.claude → _claude}/scripts/add-repo-to-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/capture-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/create-work-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/forges/gitlab.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/generate-claude-local.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-to-workspace-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sweep-references.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sync-tasks.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/workspace-diagnostics.mjs +0 -0
- /package/template/{.claude → _claude}/settings.json +0 -0
- /package/template/{.claude → _claude}/skills/aside/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/framing.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/pitfalls.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/review.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/bulk-fill-migration.py +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/forbidden-word-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/leak-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/custom.css.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Arrow.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Box.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Region.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/SectionTitle.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/tokens.ts +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/sidebars.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/spec.md.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/pause-work/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/promote/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/setup-tracker/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/sync-work/SKILL.md +0 -0
- /package/template/{.mcp.json → _mcp.json} +0 -0
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
// Source of truth: the filesystem. Hand edits are overwritten on regeneration.
|
|
10
10
|
// Gitignored files are excluded automatically. .indexignore adds prefix excludes.
|
|
11
11
|
//
|
|
12
|
-
// Canonical files honor
|
|
12
|
+
// Canonical files honor an opt-in byte budget. Each locked file declares
|
|
13
13
|
// `priority: critical | reference` in its frontmatter (default: critical).
|
|
14
14
|
// Section-level `<!-- canonical:trim --> ... <!-- canonical:end-trim -->` markers
|
|
15
|
-
// fence droppable spans inside reference files. When
|
|
16
|
-
//
|
|
17
|
-
// deterministic order. Critical files are never modified.
|
|
15
|
+
// fence droppable spans inside reference files. When a budget is set and the
|
|
16
|
+
// canonical body exceeds it, the builder trims reference files first, then
|
|
17
|
+
// stubs them, in that deterministic order. Critical files are never modified.
|
|
18
|
+
// `workspace.canonicalBudgetBytes` is off unless it holds a number: absent or
|
|
19
|
+
// null means every locked file ships in full and nothing is trimmed.
|
|
18
20
|
//
|
|
19
21
|
// Usage:
|
|
20
22
|
// node build-workspace-context.mjs --write [--root <workspace-root>]
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
// 0 — all artifacts current and canonical within budget
|
|
26
28
|
// 1 — at least one artifact missing or stale (regenerate via --write)
|
|
27
29
|
// 2 — artifacts current, but canonical body exceeds budget after trimming and stubbing
|
|
30
|
+
// (only reachable when a budget is set; off means nothing can exceed it)
|
|
28
31
|
// Stale wins over over-budget when both apply.
|
|
29
32
|
|
|
30
33
|
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, realpathSync } from 'node:fs';
|
|
@@ -301,16 +304,19 @@ export function extractCanonicalVariants({ name, rawContent }) {
|
|
|
301
304
|
}
|
|
302
305
|
|
|
303
306
|
/**
|
|
304
|
-
* Read `workspace.canonicalBudgetBytes` from `workspace.json`.
|
|
307
|
+
* Read `workspace.canonicalBudgetBytes` from `workspace.json`. The budget is
|
|
308
|
+
* opt-in — the whole always-loaded set is measured by `alwaysLoadedBudgetBytes`
|
|
309
|
+
* instead, so a canonical-only ceiling is something a workspace opts into.
|
|
305
310
|
* Returns:
|
|
306
|
-
* -
|
|
307
|
-
* - 0 when set to 0 or a negative number (treated as
|
|
308
|
-
* -
|
|
311
|
+
* - 0 when the field is absent or `null`, or workspace.json is missing (off).
|
|
312
|
+
* - 0 when set to 0 or a negative number (treated as off).
|
|
313
|
+
* - The integer value when set to a positive number.
|
|
314
|
+
* - DEFAULT_CANONICAL_BUDGET when set to a non-number (invalid, rejected as before).
|
|
309
315
|
* - DEFAULT_CANONICAL_BUDGET (with a stderr warning) when workspace.json fails to parse.
|
|
310
316
|
*/
|
|
311
317
|
export function readWorkspaceBudget(workspaceRoot) {
|
|
312
318
|
const path = join(workspaceRoot, 'workspace.json');
|
|
313
|
-
if (!existsSync(path)) return
|
|
319
|
+
if (!existsSync(path)) return 0;
|
|
314
320
|
let parsed;
|
|
315
321
|
try {
|
|
316
322
|
parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -319,9 +325,10 @@ export function readWorkspaceBudget(workspaceRoot) {
|
|
|
319
325
|
return DEFAULT_CANONICAL_BUDGET;
|
|
320
326
|
}
|
|
321
327
|
const ws = parsed && typeof parsed === 'object' ? parsed.workspace : null;
|
|
322
|
-
if (!ws || typeof ws !== 'object') return
|
|
323
|
-
if (!('canonicalBudgetBytes' in ws)) return
|
|
328
|
+
if (!ws || typeof ws !== 'object') return 0;
|
|
329
|
+
if (!('canonicalBudgetBytes' in ws)) return 0;
|
|
324
330
|
const v = ws.canonicalBudgetBytes;
|
|
331
|
+
if (v === null) return 0;
|
|
325
332
|
if (typeof v !== 'number' || !Number.isFinite(v)) return DEFAULT_CANONICAL_BUDGET;
|
|
326
333
|
if (v <= 0) return 0;
|
|
327
334
|
return Math.floor(v);
|
|
@@ -359,7 +366,8 @@ export function renderCanonicalBody(resolvedItems) {
|
|
|
359
366
|
* 4. Keep stage-3 resolution; status `over-budget`, overBy populated.
|
|
360
367
|
*
|
|
361
368
|
* Special cases:
|
|
362
|
-
* - budgetBytes <= 0
|
|
369
|
+
* - budgetBytes <= 0 (off — absent, null, 0, or negative): stage 1 always
|
|
370
|
+
* wins, selection.budgetBytes = null.
|
|
363
371
|
* - No reference items present and stage 1 fails: status `over-budget`,
|
|
364
372
|
* no transformation possible. Stderr warning is emitted.
|
|
365
373
|
*/
|
|
@@ -380,7 +388,7 @@ export function selectCanonicalContent(items, budgetBytes, opts) {
|
|
|
380
388
|
return { name: item.name, priority: item.priority, content: item.full };
|
|
381
389
|
});
|
|
382
390
|
|
|
383
|
-
//
|
|
391
|
+
// No-budget path (off because unset, or explicitly disabled).
|
|
384
392
|
if (!Number.isFinite(budgetBytes) || budgetBytes <= 0) {
|
|
385
393
|
const resolved = resolveAt(1);
|
|
386
394
|
return {
|
|
@@ -537,6 +545,8 @@ function renderCanonical(resolvedItems, selection) {
|
|
|
537
545
|
lines.push(
|
|
538
546
|
`> Budget: ${selection.budgetBytes} bytes (body); current: ${selection.currentBytes} bytes; status: ${selection.status} (${summarizeSelection(selection)}).`,
|
|
539
547
|
);
|
|
548
|
+
} else if (selection) {
|
|
549
|
+
lines.push(`> Budget: off; current: ${selection.currentBytes} bytes.`);
|
|
540
550
|
}
|
|
541
551
|
lines.push('');
|
|
542
552
|
|
|
@@ -673,7 +683,8 @@ function regenerateAll(workspaceRoot) {
|
|
|
673
683
|
* 0 — all artifacts current and canonical body is within budget.
|
|
674
684
|
* 1 — at least one artifact is missing or stale on disk. Run `--write`.
|
|
675
685
|
* 2 — artifacts are current but canonical body exceeds budget after
|
|
676
|
-
* trimming and stubbing eligible reference files
|
|
686
|
+
* trimming and stubbing eligible reference files (only reachable when
|
|
687
|
+
* a budget is set — off means nothing can exceed it). Triage via
|
|
677
688
|
* `/maintenance cleanup`. If both stale and over-budget, exit 1.
|
|
678
689
|
*
|
|
679
690
|
* `--write` always exits 0 on successful regeneration; over-budget is
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
// node chat-record.mjs --root <dir> --list
|
|
21
21
|
// node chat-record.mjs --root <dir> --read <chat-name>
|
|
22
22
|
// node chat-record.mjs --root <dir> --reconcile --session-id <id> --name <n>
|
|
23
|
+
// node chat-record.mjs --root <dir> --whoami
|
|
24
|
+
// node chat-record.mjs --root <dir> --add-task --chat <n> --branch <b> [--repo <r>] [--work-item <id>]
|
|
25
|
+
// node chat-record.mjs --root <dir> --remove-task --chat <n> (--work-item <id> | --branch <b>) [--repo <r>]
|
|
26
|
+
//
|
|
27
|
+
// --add-task / --remove-task report `action`: "added" | "updated" for
|
|
28
|
+
// --add-task, "removed" | "unchanged" for --remove-task. They also keep the
|
|
29
|
+
// older `updated` boolean / `removed` count fields — `updated` means "an
|
|
30
|
+
// existing entry was replaced", not "anything changed" — but new callers
|
|
31
|
+
// should read `action`. --whoami prints this chat's record name by matching
|
|
32
|
+
// $CLAUDE_CODE_SESSION_ID against the records' sessionId, nothing and exit
|
|
33
|
+
// 1 when there is no match: the `Chat record:` hook line can be missing
|
|
34
|
+
// after context compaction, and this is the recovery path.
|
|
23
35
|
|
|
24
36
|
import {
|
|
25
37
|
readFileSync, writeFileSync, existsSync, mkdirSync,
|
|
@@ -179,35 +191,54 @@ function resolveChatName(root, { sessionId, registryName = null } = {}) {
|
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
|
|
182
|
-
// A task is a
|
|
183
|
-
// created on demand and disappears when it
|
|
184
|
-
// durable except the issue and the commits, so
|
|
185
|
-
// pointer, never a copy of the issue.
|
|
194
|
+
// A task is a branch plus the repo it lands in, plus the tracker issue when
|
|
195
|
+
// the workspace has one. It is created on demand and disappears when it
|
|
196
|
+
// merges — nothing about it is durable except the issue and the commits, so
|
|
197
|
+
// the record holds only the pointer, never a copy of the issue.
|
|
186
198
|
//
|
|
187
199
|
// Identity is workItem + repo: the same issue can legitimately be open against
|
|
188
200
|
// two repos in a multi-repo task, and re-recording the same one must update
|
|
189
|
-
// rather than duplicate. /start-work is not always run exactly once.
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
// rather than duplicate. /start-work is not always run exactly once. A task
|
|
202
|
+
// recorded without a tracker (gh:173) carries `workItem: null` and is keyed
|
|
203
|
+
// by branch + repo instead — the branch is the only identity it has — so
|
|
204
|
+
// both add and remove take the branch for those entries.
|
|
205
|
+
function addTask(root, chatName, { workItem = null, branch, repo = null } = {}) {
|
|
192
206
|
if (!branch) throw new Error('addTask: branch is required');
|
|
193
207
|
const rec = readRecord(root, chatName);
|
|
194
208
|
if (!rec) throw new Error(`addTask: no chat record for "${chatName}"`);
|
|
195
|
-
const i = rec.tasks.findIndex(
|
|
209
|
+
const i = rec.tasks.findIndex(
|
|
210
|
+
(t) => (t.workItem ?? null) === workItem && (t.repo ?? null) === repo
|
|
211
|
+
&& (workItem !== null || t.branch === branch),
|
|
212
|
+
);
|
|
196
213
|
const task = { workItem, branch, repo };
|
|
197
214
|
if (i >= 0) rec.tasks[i] = task;
|
|
198
215
|
else rec.tasks.push(task);
|
|
199
216
|
writeRecord(root, rec);
|
|
200
|
-
return { record: rec, updated: i >= 0 };
|
|
217
|
+
return { record: rec, action: i >= 0 ? 'updated' : 'added', updated: i >= 0 };
|
|
201
218
|
}
|
|
202
219
|
|
|
203
|
-
function removeTask(root, chatName, { workItem, repo = null } = {}) {
|
|
204
|
-
if (!
|
|
220
|
+
function removeTask(root, chatName, { workItem = null, branch = null, repo = null } = {}) {
|
|
221
|
+
if (workItem === null && !branch) throw new Error('removeTask: workItem or branch is required');
|
|
205
222
|
const rec = readRecord(root, chatName);
|
|
206
223
|
if (!rec) throw new Error(`removeTask: no chat record for "${chatName}"`);
|
|
207
224
|
const before = rec.tasks.length;
|
|
208
|
-
rec.tasks = rec.tasks.filter(
|
|
225
|
+
rec.tasks = rec.tasks.filter(
|
|
226
|
+
(t) => !((t.workItem ?? null) === workItem && (t.repo ?? null) === repo
|
|
227
|
+
&& (workItem !== null || t.branch === branch)),
|
|
228
|
+
);
|
|
209
229
|
writeRecord(root, rec);
|
|
210
|
-
return { record: rec, removed: before - rec.tasks.length };
|
|
230
|
+
return { record: rec, action: before - rec.tasks.length > 0 ? 'removed' : 'unchanged', removed: before - rec.tasks.length };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// The record name for the chat running now. The SessionStart hook injects
|
|
234
|
+
// a `Chat record:` line, but compaction can drop it; the sessionId the
|
|
235
|
+
// hook keyed on is stable, so matching it against the records recovers the
|
|
236
|
+
// name without guessing. Null when the env var is unset or nothing matches.
|
|
237
|
+
function whoami(root, { env = process.env } = {}) {
|
|
238
|
+
const sid = env.CLAUDE_CODE_SESSION_ID;
|
|
239
|
+
if (!sid) return null;
|
|
240
|
+
const mine = listRecords(root).find((r) => r.sessionId === sid);
|
|
241
|
+
return mine ? mine.chat : null;
|
|
211
242
|
}
|
|
212
243
|
|
|
213
244
|
// Scope is what a chat declares it owns. The Aug 26 coordination burst had
|
|
@@ -230,6 +261,7 @@ function parseArgs(argv) {
|
|
|
230
261
|
if (a === '--list') { args.mode = 'list'; continue; }
|
|
231
262
|
if (a === '--read') { args.mode = 'read'; args.chat = rest[++i]; continue; }
|
|
232
263
|
if (a === '--reconcile') { args.mode = 'reconcile'; continue; }
|
|
264
|
+
if (a === '--whoami') { args.mode = 'whoami'; continue; }
|
|
233
265
|
if (a === '--add-task') { args.mode = 'add-task'; continue; }
|
|
234
266
|
if (a === '--remove-task') { args.mode = 'remove-task'; continue; }
|
|
235
267
|
if (a === '--chat') { args.chat = rest[++i]; continue; }
|
|
@@ -240,28 +272,37 @@ function parseArgs(argv) {
|
|
|
240
272
|
if (a === '--name') { args.name = rest[++i]; continue; }
|
|
241
273
|
throw new Error(`unknown argument: ${a}`);
|
|
242
274
|
}
|
|
243
|
-
if (!args.mode) throw new Error('one of --list, --read <chat>, --reconcile is required');
|
|
275
|
+
if (!args.mode) throw new Error('one of --list, --read <chat>, --reconcile, --whoami is required');
|
|
244
276
|
if (args.mode === 'reconcile' && (!args.sessionId || !args.name)) {
|
|
245
277
|
throw new Error('--reconcile requires --session-id and --name');
|
|
246
278
|
}
|
|
247
|
-
if (args.mode === 'add-task' && (!args.chat || !args.
|
|
248
|
-
throw new Error('--add-task requires --chat
|
|
279
|
+
if (args.mode === 'add-task' && (!args.chat || !args.branch)) {
|
|
280
|
+
throw new Error('--add-task requires --chat and --branch (--work-item is optional — omitted without a tracker)');
|
|
249
281
|
}
|
|
250
|
-
if (args.mode === 'remove-task' && (!args.chat || !args.workItem)) {
|
|
251
|
-
throw new Error('--remove-task requires --chat and --work-item');
|
|
282
|
+
if (args.mode === 'remove-task' && (!args.chat || (!args.workItem && !args.branch))) {
|
|
283
|
+
throw new Error('--remove-task requires --chat and --work-item or --branch');
|
|
252
284
|
}
|
|
253
285
|
return args;
|
|
254
286
|
}
|
|
255
287
|
|
|
256
288
|
function main() {
|
|
257
289
|
const args = parseArgs(process.argv);
|
|
290
|
+
if (args.mode === 'whoami') {
|
|
291
|
+
// A bare name, not JSON: the caller wants something to put on a command
|
|
292
|
+
// line. No match prints nothing and exits 1 — the caller decides what
|
|
293
|
+
// an unidentified chat means.
|
|
294
|
+
const name = whoami(args.root);
|
|
295
|
+
if (name === null) process.exit(1);
|
|
296
|
+
process.stdout.write(`${name}\n`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
258
299
|
let out;
|
|
259
300
|
if (args.mode === 'list') out = listRecords(args.root);
|
|
260
301
|
else if (args.mode === 'read') out = readRecord(args.root, args.chat);
|
|
261
302
|
else if (args.mode === 'add-task') {
|
|
262
|
-
out = addTask(args.root, args.chat, { workItem: args.workItem, branch: args.branch, repo: args.repo });
|
|
303
|
+
out = addTask(args.root, args.chat, { workItem: args.workItem ?? null, branch: args.branch, repo: args.repo });
|
|
263
304
|
} else if (args.mode === 'remove-task') {
|
|
264
|
-
out = removeTask(args.root, args.chat, { workItem: args.workItem, repo: args.repo });
|
|
305
|
+
out = removeTask(args.root, args.chat, { workItem: args.workItem ?? null, branch: args.branch, repo: args.repo });
|
|
265
306
|
} else out = reconcile(args.root, { sessionId: args.sessionId, name: args.name });
|
|
266
307
|
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
267
308
|
}
|
|
@@ -278,5 +319,5 @@ if (isMainModule(import.meta.url)) {
|
|
|
278
319
|
export {
|
|
279
320
|
recordPath, drawerPath, emptyRecord, readRecord, writeRecord,
|
|
280
321
|
listRecords, reconcile, parseArgs, readSessionRegistry, resolveChatName,
|
|
281
|
-
addTask, removeTask, setScope, CHATS_DIR,
|
|
322
|
+
addTask, removeTask, setScope, whoami, CHATS_DIR,
|
|
282
323
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Classify an upgrade payload's files against the workspace so /workspace-update
|
|
3
|
+
// can batch the safe cases and ask only where a decision is needed.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node classify-update.mjs [--root <dir>] [--payload <dir>]
|
|
7
|
+
//
|
|
8
|
+
// --root workspace root; defaults to the current working directory (never
|
|
9
|
+
// derived from this script's location — the upgrade payload runs
|
|
10
|
+
// this file from <workspace>/.workspace-update/.claude/scripts/)
|
|
11
|
+
// --payload the staged payload; defaults to <root>/.workspace-update
|
|
12
|
+
//
|
|
13
|
+
// Prints JSON: { "new": [...], "identical": [...], "differs": [...] }
|
|
14
|
+
// new — no installed counterpart; safe to batch-apply after one confirm
|
|
15
|
+
// identical — installed file already equals the payload byte-for-byte
|
|
16
|
+
// differs — installed file differs; needs a per-file decision
|
|
17
|
+
//
|
|
18
|
+
// Only verbatim-installed files are classified: everything under .claude/,
|
|
19
|
+
// plus .mcp.json and .claudeignore. The payload's templates (*.tmpl, which
|
|
20
|
+
// install with {{project-name}} substitution), _gitignore (merged line-by-line
|
|
21
|
+
// into the workspace's .gitignore), and .manifest.json (payload metadata) are
|
|
22
|
+
// handled by their own steps in /workspace-update and are excluded here.
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
readdirSync,
|
|
28
|
+
statSync,
|
|
29
|
+
realpathSync,
|
|
30
|
+
} from 'node:fs';
|
|
31
|
+
import { join, resolve } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
function isMainModule(metaUrl) {
|
|
35
|
+
if (!process.argv[1]) return false;
|
|
36
|
+
try {
|
|
37
|
+
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(process.argv[1]);
|
|
38
|
+
} catch { return false; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const args = { root: process.cwd(), payload: null };
|
|
43
|
+
for (let i = 2; i < argv.length; i++) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
if (a === '--root') args.root = argv[++i];
|
|
46
|
+
else if (a === '--payload') args.payload = argv[++i];
|
|
47
|
+
else throw new Error(`Unknown arg: ${a}`);
|
|
48
|
+
}
|
|
49
|
+
return args;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Payload-relative paths that install verbatim at the same relative path.
|
|
53
|
+
// Everything else in the payload is a template or metadata handled elsewhere.
|
|
54
|
+
const VERBATIM_ROOTS = ['.claude', '.mcp.json', '.claudeignore'];
|
|
55
|
+
|
|
56
|
+
function isClassified(payloadRelPath) {
|
|
57
|
+
const first = payloadRelPath.split('/')[0];
|
|
58
|
+
return VERBATIM_ROOTS.includes(first);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function* walkFiles(dir, prefix = '') {
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = readdirSync(dir).sort();
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const name of entries) {
|
|
69
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
70
|
+
const full = join(dir, name);
|
|
71
|
+
let st;
|
|
72
|
+
try { st = statSync(full); } catch { continue; }
|
|
73
|
+
if (st.isDirectory()) yield* walkFiles(full, rel);
|
|
74
|
+
else if (st.isFile()) yield rel;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function classifyUpdate({ root, payload }) {
|
|
79
|
+
const absRoot = resolve(root);
|
|
80
|
+
const absPayload = resolve(payload ?? join(absRoot, '.workspace-update'));
|
|
81
|
+
if (!existsSync(absPayload)) {
|
|
82
|
+
throw new Error(`No payload found at ${absPayload} — run npx @ulysses-ai/create-workspace --upgrade first`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = { new: [], identical: [], differs: [] };
|
|
86
|
+
for (const rel of walkFiles(absPayload)) {
|
|
87
|
+
if (!isClassified(rel)) continue;
|
|
88
|
+
const installed = join(absRoot, rel);
|
|
89
|
+
if (!existsSync(installed)) {
|
|
90
|
+
result.new.push(rel);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const payloadBytes = readFileSync(join(absPayload, rel));
|
|
94
|
+
const installedBytes = readFileSync(installed);
|
|
95
|
+
if (Buffer.compare(payloadBytes, installedBytes) === 0) {
|
|
96
|
+
result.identical.push(rel);
|
|
97
|
+
} else {
|
|
98
|
+
result.differs.push(rel);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function main() {
|
|
105
|
+
const args = parseArgs(process.argv);
|
|
106
|
+
const result = classifyUpdate({ root: args.root, payload: args.payload });
|
|
107
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isMainModule(import.meta.url)) {
|
|
111
|
+
try {
|
|
112
|
+
main();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
process.stderr.write(`classify-update: ${err.message}\n`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -146,9 +146,11 @@ if (repos.length === 0) {
|
|
|
146
146
|
if (found.length > 0) {
|
|
147
147
|
repos = found;
|
|
148
148
|
discovered = true;
|
|
149
|
+
// Informational wording: discovery is a normal success path here
|
|
150
|
+
// (gh:172) — not a warning about the tracker.
|
|
149
151
|
skipped.push({
|
|
150
152
|
step: 'discovery',
|
|
151
|
-
reason: `
|
|
153
|
+
reason: `repos discovered from disk: ${found.length} (${found.join(', ')})`,
|
|
152
154
|
});
|
|
153
155
|
}
|
|
154
156
|
} catch (err) {
|
|
@@ -209,7 +211,7 @@ if (!branch && existsSync(wsWorktree)) {
|
|
|
209
211
|
const res = git(wsWorktree, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
210
212
|
if (res.ok && res.out.trim() !== '' && res.out.trim() !== 'HEAD') {
|
|
211
213
|
branch = res.out.trim();
|
|
212
|
-
skipped.push({ step: 'discovery', reason: `
|
|
214
|
+
skipped.push({ step: 'discovery', reason: `branch discovered from the worktree: ${branch}` });
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
217
|
|