@ulysses-ai/create-workspace 0.18.0-beta.0 → 0.19.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/package.json +1 -1
- 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 +37 -4
- 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/task-pr.mjs +447 -0
- 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 +13 -69
- 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/release/SKILL.md +6 -2
- package/template/{.claude → _claude}/skills/start-work/SKILL.md +2 -2
- package/template/workspace.json.tmpl +1 -1
- /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/freshness.mjs +0 -0
- /package/template/{.claude → _claude}/lib/registry-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/cleanup-work-session.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-canonical-priority.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-claude-md-freshness-include.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-sessions.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/task-worktree.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/migrate-sessions/SKILL.md +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/{.claude → _claude}/skills/workspace-init/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/workspace-update/SKILL.md +0 -0
- /package/template/{.mcp.json → _mcp.json} +0 -0
package/lib/init.mjs
CHANGED
|
@@ -20,7 +20,10 @@ export async function initWorkspace(targetDir) {
|
|
|
20
20
|
const { toVersion, payloadDir } = stagePayload(targetDir, { action: 'init' });
|
|
21
21
|
console.log(` Staged template payload (v${toVersion})`);
|
|
22
22
|
|
|
23
|
-
// Install only the bootstrap skills needed to complete initialization
|
|
23
|
+
// Install only the bootstrap skills needed to complete initialization.
|
|
24
|
+
// The payload keeps live names (.claude/, .mcp.json) — stagePayload()
|
|
25
|
+
// maps them back from the template's inert names so the staged layout
|
|
26
|
+
// stays readable by skills already installed in the workspace.
|
|
24
27
|
const bootstrapSkills = ['workspace-init', 'workspace-update'];
|
|
25
28
|
const payloadSkills = join(payloadDir, '.claude', 'skills');
|
|
26
29
|
const targetSkills = join(targetDir, '.claude', 'skills');
|
package/lib/payload.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lib/payload.mjs
|
|
2
|
-
import { existsSync, cpSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, cpSync, rmSync, mkdirSync, writeFileSync, readFileSync, renameSync } from 'fs';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
|
|
@@ -22,6 +22,23 @@ export function stagePayload(targetDir, { action, fromVersion = null }) {
|
|
|
22
22
|
// Copy template to payload directory
|
|
23
23
|
cpSync(TEMPLATE_DIR, payloadDir, { recursive: true });
|
|
24
24
|
|
|
25
|
+
// The template stores .claude/ and .mcp.json under the inert names
|
|
26
|
+
// _claude/ and _mcp.json (Claude Code protects the live names from
|
|
27
|
+
// headless edits). The staged payload is an on-disk format shared with
|
|
28
|
+
// the /workspace-update skill ALREADY INSTALLED in the workspace, which
|
|
29
|
+
// reads .workspace-update/.claude/... — so it must keep the live names
|
|
30
|
+
// exactly as older package versions staged them. _gitignore stays inert:
|
|
31
|
+
// the skills have always merged it under that name.
|
|
32
|
+
for (const [from, to] of [
|
|
33
|
+
['_claude', '.claude'],
|
|
34
|
+
['_mcp.json', '.mcp.json'],
|
|
35
|
+
]) {
|
|
36
|
+
const src = join(payloadDir, from);
|
|
37
|
+
if (existsSync(src)) {
|
|
38
|
+
renameSync(src, join(payloadDir, to));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
// Write manifest
|
|
26
43
|
const manifest = {
|
|
27
44
|
action,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Unit tests for payload.mjs
|
|
3
|
+
// Run: node lib/payload.test.mjs
|
|
4
|
+
//
|
|
5
|
+
// The staged .workspace-update/ payload is an on-disk format shared with
|
|
6
|
+
// the /workspace-update skill already installed in existing workspaces,
|
|
7
|
+
// which reads .workspace-update/.claude/... and .workspace-update/.mcp.json.
|
|
8
|
+
// These tests pin that layout even though template/ itself stores the
|
|
9
|
+
// protected paths under the inert names _claude/ and _mcp.json.
|
|
10
|
+
import { stagePayload, cleanPayload } from './payload.mjs';
|
|
11
|
+
import { mkdtempSync, rmSync, existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { tmpdir } from 'os';
|
|
14
|
+
|
|
15
|
+
let failed = 0;
|
|
16
|
+
let passed = 0;
|
|
17
|
+
function check(label, ok) {
|
|
18
|
+
if (ok) { passed++; } else {
|
|
19
|
+
failed++;
|
|
20
|
+
console.error(` FAIL: ${label}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'payload-test-'));
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const { payloadDir, toVersion } = stagePayload(root, { action: 'init' });
|
|
28
|
+
|
|
29
|
+
check('payload staged at .workspace-update/', existsSync(payloadDir));
|
|
30
|
+
check('manifest written', existsSync(join(payloadDir, '.manifest.json')));
|
|
31
|
+
check('returns the package version', typeof toVersion === 'string' && toVersion.length > 0);
|
|
32
|
+
|
|
33
|
+
// The payload keeps the live names older workspaces' skills expect
|
|
34
|
+
check('.claude/ staged under live name', existsSync(join(payloadDir, '.claude')));
|
|
35
|
+
check('.claude/skills/workspace-update/SKILL.md staged', existsSync(join(payloadDir, '.claude', 'skills', 'workspace-update', 'SKILL.md')));
|
|
36
|
+
check('.claude/settings.json staged', existsSync(join(payloadDir, '.claude', 'settings.json')));
|
|
37
|
+
check('.mcp.json staged under live name', existsSync(join(payloadDir, '.mcp.json')));
|
|
38
|
+
check('_gitignore stays inert (skills merge it under that name)', existsSync(join(payloadDir, '_gitignore')));
|
|
39
|
+
|
|
40
|
+
// The template's inert names must not leak into the payload
|
|
41
|
+
check('no _claude/ in payload', !existsSync(join(payloadDir, '_claude')));
|
|
42
|
+
check('no _mcp.json in payload', !existsSync(join(payloadDir, '_mcp.json')));
|
|
43
|
+
|
|
44
|
+
// cleanPayload removes the staging directory
|
|
45
|
+
cleanPayload(root);
|
|
46
|
+
check('cleanPayload removes .workspace-update/', !existsSync(payloadDir));
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(root, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (failed > 0) {
|
|
52
|
+
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
console.log(`${passed} checks passed`);
|
package/lib/scaffold.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cpSync, mkdirSync, readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
|
|
1
|
+
import { cpSync, mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, rmSync, statSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
@@ -26,11 +26,28 @@ export async function scaffold(answers) {
|
|
|
26
26
|
// scripts and hooks first need them — we do NOT pre-create them here.
|
|
27
27
|
mkdirSync(join(directory, 'workspace-context', 'locked'), { recursive: true });
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// The template stores some files under inert names — _gitignore,
|
|
30
|
+
// _claude/, _mcp.json — because Claude Code treats .claude/ directories
|
|
31
|
+
// and .mcp.json files as protected paths and would block automated edits
|
|
32
|
+
// to the template. Install each under its live name in the target.
|
|
33
|
+
const inertNames = [
|
|
34
|
+
{ from: '_gitignore', to: '.gitignore' },
|
|
35
|
+
{ from: '_claude', to: '.claude' },
|
|
36
|
+
{ from: '_mcp.json', to: '.mcp.json' },
|
|
37
|
+
];
|
|
38
|
+
for (const { from, to } of inertNames) {
|
|
39
|
+
const src = join(directory, from);
|
|
40
|
+
const dest = join(directory, to);
|
|
41
|
+
if (!existsSync(src)) continue;
|
|
42
|
+
if (existsSync(dest) && statSync(src).isDirectory()) {
|
|
43
|
+
// Pre-populated target: merge into the existing directory — template
|
|
44
|
+
// files overwrite same-named ones, extra local files survive — then
|
|
45
|
+
// drop the inert copy.
|
|
46
|
+
cpSync(src, dest, { recursive: true });
|
|
47
|
+
rmSync(src, { recursive: true });
|
|
48
|
+
} else {
|
|
49
|
+
renameSync(src, dest);
|
|
50
|
+
}
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
// Process CLAUDE.md template
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// End-to-end test for scaffold.mjs
|
|
3
|
+
// Run: node lib/scaffold.test.mjs
|
|
4
|
+
//
|
|
5
|
+
// The template stores .gitignore, .claude/, and .mcp.json under the inert
|
|
6
|
+
// names _gitignore, _claude/, and _mcp.json (Claude Code protects the live
|
|
7
|
+
// names from headless edits). scaffold() must install every one under its
|
|
8
|
+
// live name and leave no inert leftovers behind.
|
|
9
|
+
import { scaffold } from './scaffold.mjs';
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
13
|
+
|
|
14
|
+
let failed = 0;
|
|
15
|
+
let passed = 0;
|
|
16
|
+
function check(label, ok) {
|
|
17
|
+
if (ok) { passed++; } else {
|
|
18
|
+
failed++;
|
|
19
|
+
console.error(` FAIL: ${label}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const directory = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await scaffold({
|
|
27
|
+
name: 'demo',
|
|
28
|
+
directory,
|
|
29
|
+
repos: [],
|
|
30
|
+
userName: 'tester',
|
|
31
|
+
activateRules: [],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Live names installed
|
|
35
|
+
check('.claude/ directory installed', existsSync(join(directory, '.claude')));
|
|
36
|
+
check('.claude/settings.json installed', existsSync(join(directory, '.claude', 'settings.json')));
|
|
37
|
+
check('.claude/settings.local.json written', existsSync(join(directory, '.claude', 'settings.local.json')));
|
|
38
|
+
check('.claude/rules/ installed', existsSync(join(directory, '.claude', 'rules')));
|
|
39
|
+
check('.claude/rules/coherent-revisions.md installed', existsSync(join(directory, '.claude', 'rules', 'coherent-revisions.md')));
|
|
40
|
+
check('.mcp.json installed', existsSync(join(directory, '.mcp.json')));
|
|
41
|
+
check('.gitignore installed', existsSync(join(directory, '.gitignore')));
|
|
42
|
+
|
|
43
|
+
// .mcp.json is the template's payload, unmodified
|
|
44
|
+
const mcp = JSON.parse(readFileSync(join(directory, '.mcp.json'), 'utf8'));
|
|
45
|
+
check('.mcp.json parses with mcpServers object', typeof mcp.mcpServers === 'object' && mcp.mcpServers !== null);
|
|
46
|
+
|
|
47
|
+
// Inert names are gone
|
|
48
|
+
check('no _claude/ leftover', !existsSync(join(directory, '_claude')));
|
|
49
|
+
check('no _mcp.json leftover', !existsSync(join(directory, '_mcp.json')));
|
|
50
|
+
check('no _gitignore leftover', !existsSync(join(directory, '_gitignore')));
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(directory, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (failed > 0) {
|
|
56
|
+
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
console.log(`${passed} checks passed`);
|
package/package.json
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/.claude/skills/**"
|
|
4
|
+
- "**/.claude/scripts/**"
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
Activate this rule if the workspace creates PRs, watches CI runs, or interacts with releases from skills. Sibling to `work-item-tracking.md` (which covers issues); together they cover everything a workspace does against a code-hosting forge.
|
|
2
8
|
|
|
3
9
|
# Forge Operations
|
|
@@ -32,6 +32,10 @@ not need to be in context while editing documentation.
|
|
|
32
32
|
node .claude/scripts/context-footprint.mjs --root . --add <bytes> --as <destination>
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
A rule that applies only to certain files should carry `paths:` frontmatter — it costs nothing
|
|
36
|
+
until a matching file is touched — and the same script checks the live total against
|
|
37
|
+
`workspace.alwaysLoadedBudgetBytes`.
|
|
38
|
+
|
|
35
39
|
## The canonical test
|
|
36
40
|
|
|
37
41
|
Canonical describes what *is* and what *to do*, never what *to think*.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "work-sessions/**"
|
|
4
|
+
- "**/session.md"
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# Task List Mirroring
|
|
2
8
|
|
|
3
9
|
The Claude Code `TodoWrite` checklist is a live mirror of the workspace lifecycle. The durable backing store is a `## Tasks` section in `session.md`, round-tripped by `.claude/scripts/sync-tasks.mjs`. This rule defines the contract.
|
|
@@ -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> --work-item <id> --branch <b> [--repo <r>]
|
|
25
|
+
// node chat-record.mjs --root <dir> --remove-task --chat <n> --work-item <id> [--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,
|
|
@@ -197,7 +209,7 @@ function addTask(root, chatName, { workItem, branch, repo = null } = {}) {
|
|
|
197
209
|
if (i >= 0) rec.tasks[i] = task;
|
|
198
210
|
else rec.tasks.push(task);
|
|
199
211
|
writeRecord(root, rec);
|
|
200
|
-
return { record: rec, updated: i >= 0 };
|
|
212
|
+
return { record: rec, action: i >= 0 ? 'updated' : 'added', updated: i >= 0 };
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
function removeTask(root, chatName, { workItem, repo = null } = {}) {
|
|
@@ -207,7 +219,18 @@ function removeTask(root, chatName, { workItem, repo = null } = {}) {
|
|
|
207
219
|
const before = rec.tasks.length;
|
|
208
220
|
rec.tasks = rec.tasks.filter((t) => !(t.workItem === workItem && (t.repo ?? null) === repo));
|
|
209
221
|
writeRecord(root, rec);
|
|
210
|
-
return { record: rec, removed: before - rec.tasks.length };
|
|
222
|
+
return { record: rec, action: before - rec.tasks.length > 0 ? 'removed' : 'unchanged', removed: before - rec.tasks.length };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The record name for the chat running now. The SessionStart hook injects
|
|
226
|
+
// a `Chat record:` line, but compaction can drop it; the sessionId the
|
|
227
|
+
// hook keyed on is stable, so matching it against the records recovers the
|
|
228
|
+
// name without guessing. Null when the env var is unset or nothing matches.
|
|
229
|
+
function whoami(root, { env = process.env } = {}) {
|
|
230
|
+
const sid = env.CLAUDE_CODE_SESSION_ID;
|
|
231
|
+
if (!sid) return null;
|
|
232
|
+
const mine = listRecords(root).find((r) => r.sessionId === sid);
|
|
233
|
+
return mine ? mine.chat : null;
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
// Scope is what a chat declares it owns. The Aug 26 coordination burst had
|
|
@@ -230,6 +253,7 @@ function parseArgs(argv) {
|
|
|
230
253
|
if (a === '--list') { args.mode = 'list'; continue; }
|
|
231
254
|
if (a === '--read') { args.mode = 'read'; args.chat = rest[++i]; continue; }
|
|
232
255
|
if (a === '--reconcile') { args.mode = 'reconcile'; continue; }
|
|
256
|
+
if (a === '--whoami') { args.mode = 'whoami'; continue; }
|
|
233
257
|
if (a === '--add-task') { args.mode = 'add-task'; continue; }
|
|
234
258
|
if (a === '--remove-task') { args.mode = 'remove-task'; continue; }
|
|
235
259
|
if (a === '--chat') { args.chat = rest[++i]; continue; }
|
|
@@ -240,7 +264,7 @@ function parseArgs(argv) {
|
|
|
240
264
|
if (a === '--name') { args.name = rest[++i]; continue; }
|
|
241
265
|
throw new Error(`unknown argument: ${a}`);
|
|
242
266
|
}
|
|
243
|
-
if (!args.mode) throw new Error('one of --list, --read <chat>, --reconcile is required');
|
|
267
|
+
if (!args.mode) throw new Error('one of --list, --read <chat>, --reconcile, --whoami is required');
|
|
244
268
|
if (args.mode === 'reconcile' && (!args.sessionId || !args.name)) {
|
|
245
269
|
throw new Error('--reconcile requires --session-id and --name');
|
|
246
270
|
}
|
|
@@ -255,6 +279,15 @@ function parseArgs(argv) {
|
|
|
255
279
|
|
|
256
280
|
function main() {
|
|
257
281
|
const args = parseArgs(process.argv);
|
|
282
|
+
if (args.mode === 'whoami') {
|
|
283
|
+
// A bare name, not JSON: the caller wants something to put on a command
|
|
284
|
+
// line. No match prints nothing and exits 1 — the caller decides what
|
|
285
|
+
// an unidentified chat means.
|
|
286
|
+
const name = whoami(args.root);
|
|
287
|
+
if (name === null) process.exit(1);
|
|
288
|
+
process.stdout.write(`${name}\n`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
258
291
|
let out;
|
|
259
292
|
if (args.mode === 'list') out = listRecords(args.root);
|
|
260
293
|
else if (args.mode === 'read') out = readRecord(args.root, args.chat);
|
|
@@ -278,5 +311,5 @@ if (isMainModule(import.meta.url)) {
|
|
|
278
311
|
export {
|
|
279
312
|
recordPath, drawerPath, emptyRecord, readRecord, writeRecord,
|
|
280
313
|
listRecords, reconcile, parseArgs, readSessionRegistry, resolveChatName,
|
|
281
|
-
addTask, removeTask, setScope, CHATS_DIR,
|
|
314
|
+
addTask, removeTask, setScope, whoami, CHATS_DIR,
|
|
282
315
|
};
|