Skip to content

feat: worktree coordination registry — PARALLEL-01#113

Open
PatrickSys wants to merge 4 commits into
mainfrom
feat/v2-registry
Open

feat: worktree coordination registry — PARALLEL-01#113
PatrickSys wants to merge 4 commits into
mainfrom
feat/v2-registry

Conversation

@PatrickSys
Copy link
Copy Markdown
Owner

@PatrickSys PatrickSys commented May 13, 2026

Summary

Delivers the worktree coordination registry for the v2.0.0 parallel execution milestone (PARALLEL-01). Phases running in parallel worktrees can now record open/closed/crashed lease state through a lightweight JSON store, enabling the write-set advisory layer and crash-recovery role introduced in subsequent phases.

What ships

  • bin/lib/registry.mjs — core API: openRegistry, grantLease, closeLease, crashLease, clearLease, listLeases, getLease, plus exported helpers registryPath, registryTmpPath, registryExists. Write path is atomic via per-PID registry.json.<pid>.tmp + renameSync; corrupt files are quarantined to registry.json.broken-<ts>; a read-after-write fingerprint warning surfaces lost updates between concurrent writers on stderr. Windows EPERM/EBUSY on rename is handled by bounded retry. Zero external dependencies.
  • bin/lib/registry-commands.mjs — four CLI handlers (registry-list, registry-show, registry-clear, registry-crash placeholder). Workspace root resolved via resolveWorkspaceContext so commands work from subdirectories. Error messages strip internal function prefixes.
  • bin/gsdd.mjs — four new commands registered alphabetically; help text updated.
  • bin/lib/closeout-report.mjs — registry section surfaced when a registry file is present. Only leases for other phases tag as [BLOCK] when closing a given phase; the closing phase's own open lease tags as [INFO].
  • bin/lib/phase.mjsgsdd phase-status N done gains a programmatic artifact gate: refuses the transition unless NN-PLAN-CHECK.md, NN-VERIFICATION.md, and a recent governance doc-sync exist. --force --reason <text> is the audited escape hatch.
  • Tests — 23 registry tests (unit + cross-platform parent-kills-child durability fixture with .tmp orphan assertion + corrupt-JSON quarantine + per-PID concurrent-write isolation + subdirectory CWD resolution + state-guard coverage), 12 closeout-report tests (incl. foreign-phase blocking distinction), 8 new phase-status gate tests covering refusal paths and override path.

Design basis

D64 in distilled/DESIGN.md (Track C: JSON + atomic rename). Evidence with verifiable URLs and commit refs in distilled/EVIDENCE-INDEX.md across the three required research categories: spec frameworks (GSD confirmed negative; OpenSpec discovery YAML only; LeanSpec has no coordination state), orchestrators (OpenHands per-event JSON files at commit cae76e54; MetaGPT team.json; Conductor OSS JSON-blob wire format), industry guidance (Anthropic Claude Code race-condition fix consensus on tmp+rename; Cursor 2.0 .cursor/worktrees.json; OpenAI Codex CLI SQLite recorded as the upgrade target; GitHub Copilot Coding Agent delegates to git worktrees).

Upgrade path: revisit SQLite when automated orchestration with concurrent multi-writer requirements lands.

Test plan

  • node --test tests/gsdd.registry.test.cjs → 23/23 pass (durability fixture passes on Windows NTFS; concurrent-write per-PID isolation verified; corrupt-JSON quarantine verified)
  • node --test tests/gsdd.closeout-report.test.cjs → 12/12 pass (foreign-phase blocking distinction verified)
  • node --test tests/phase.test.cjs → 164/164 pass (8 new artifact-gate tests included; pre-existing scenarios unaffected)
  • npm run test:gsdd → 792/795 pass; 3 pre-existing README invariant failures are unchanged baseline
  • node bin/gsdd.mjs registry-list → "No leases found." on a fresh workspace
  • node bin/gsdd.mjs registry-list from a nested subdirectory → resolves up to the workspace root
  • node bin/gsdd.mjs registry-show 99 → exits 1 with clear message
  • node bin/gsdd.mjs registry-clear 99 → exits 1 with usage error (no lease) and no internal-function-name leak
  • node bin/gsdd.mjs registry-crash 99 → exits 1 with deferral message until the next milestone wires the consumer
  • node bin/gsdd.mjs closeout-report --json → output contains no registry key when registry file absent
  • Zero new runtime dependencies (package.json dependencies: {})

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 881f74acbb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bin/lib/registry-commands.mjs Outdated

export async function cmdRegistryList(...args) {
const jsonMode = args.includes('--json');
const leases = listLeases(process.cwd());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve registry commands against workspace root

registry-list, registry-show, and registry-clear all read from process.cwd() instead of resolving the workspace root, so running them from a nested directory silently targets <subdir>/.planning/.local/registry.json and can report “No leases found” even when the real root registry has active leases. This breaks the coordination safety model in common workflows where users run commands from subfolders; these handlers should use resolveWorkspaceContext(args) like other CLI commands.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. All three handlers in bin/lib/registry-commands.mjs now resolve the workspace root via resolveWorkspaceContext(args) (extracted into a resolveRegistryRoot helper). New regression test registry commands resolve workspace root from a nested cwd confirms a subdir invocation walks up to the workspace .planning/ marker. Enables --workspace-root <path> and GSDD_WORKSPACE_ROOT overrides for free.

Comment thread bin/lib/registry.mjs Outdated
const p = registryPath(workspaceRoot);
if (!existsSync(p)) return emptyRegistry();
try {
return JSON.parse(readFileSync(p, 'utf8'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate parsed registry schema before lease operations

The parser returns raw JSON without checking that leases is an array, but downstream operations call data.leases.find(...)/filter(...) unguarded. If registry.json is valid JSON but missing or malformed (for example { "schema_version": 1 }), commands like registry-show and lease mutations crash with an uncaught TypeError instead of handling corruption gracefully. Normalize invalid shapes to emptyRegistry() (or reject with a controlled error) in readRegistry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Normalization is centralized in readRegistry so all six callers (grantLease/closeLease/crashLease/getLease/listLeases/clearLease) inherit the guarantee in one place: parse errors and any non-array leases shape quarantine the offending file to registry.json.broken-<ts> with a stderr warning, then return an empty registry. Three new tests cover the parse-error path, the wrong-shape path ({schema_version:1} with no leases), and the empty-file edge case.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a new worktree coordination registry using a JSON-based file system approach with atomic renames, replacing the previously considered SQLite-based implementation. The changes include a new registry module, CLI commands for managing leases, integration into the closeout report, and comprehensive test coverage. The reviewer suggested exporting the registry path logic to improve modularity and reduce redundant path construction, which I agree would be a beneficial refactoring.

Comment thread bin/lib/registry.mjs Outdated
Comment on lines +14 to +16
function registryPath(workspaceRoot) {
return join(workspaceRoot, '.planning', '.local', 'registry.json');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Exporting registryPath allows other modules to use this path logic, avoiding redundant path construction and reducing the need for node:path imports in dependent modules.

Suggested change
function registryPath(workspaceRoot) {
return join(workspaceRoot, '.planning', '.local', 'registry.json');
}
export function registryPath(workspaceRoot) {
return join(workspaceRoot, '.planning', '.local', 'registry.json');
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. registryPath, registryTmpPath, and a narrow registryExists(workspaceRoot) helper are now exported from bin/lib/registry.mjs. The test file and bin/lib/closeout-report.mjs consume them directly instead of duplicating the path-construction string.

Comment thread bin/lib/closeout-report.mjs Outdated
Comment on lines +55 to +62
const { listLeases } = await import('./registry.mjs');
const leases = listLeases(workspaceRoot);
// If no leases exist and no file exists, listLeases returns [].
// We need to know whether the registry file itself exists.
// Use the fs import to check — only add the key if the file is present.
const { existsSync } = await import('node:fs');
const { join } = await import('node:path');
const registryFile = join(workspaceRoot, '.planning', '.local', 'registry.json');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import registryPath from registry.mjs to avoid re-implementing the path logic and to remove the unnecessary node:path import inside the function.

Suggested change
const { listLeases } = await import('./registry.mjs');
const leases = listLeases(workspaceRoot);
// If no leases exist and no file exists, listLeases returns [].
// We need to know whether the registry file itself exists.
// Use the fs import to check — only add the key if the file is present.
const { existsSync } = await import('node:fs');
const { join } = await import('node:path');
const registryFile = join(workspaceRoot, '.planning', '.local', 'registry.json');
const { listLeases, registryPath } = await import('./registry.mjs');
const leases = listLeases(workspaceRoot);
// If no leases exist and no file exists, listLeases returns [].
// We need to know whether the registry file itself exists.
// Use the fs import to check — only add the key if the file is present.
const { existsSync } = await import('node:fs');
const registryFile = registryPath(workspaceRoot);
if (!existsSync(registryFile)) return null;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. buildRegistrySectionSafe imports registryExists from ./registry.mjs — the inline existsSync + join and the dynamic node:fs/node:path imports inside that function are gone.

Test User added 4 commits May 13, 2026 13:29
Adds a JSON-backed registry at .planning/.local/registry.json that records
per-phase lease state (open/closed/crashed) and survives session restart.
Writes are atomic via per-PID tmp filename + renameSync; corrupt files are
quarantined to registry.json.broken-<ts>; a read-after-write fingerprint
warning surfaces lost updates between concurrent writers on stderr.

The CLI exposes four hyphenated commands: registry-list, registry-show,
registry-clear (with --force gate for open leases), and registry-crash
(placeholder for the next milestone). Workspace root is resolved via the
shared resolveWorkspaceContext helper so commands work from nested
directories. closeout-report gains a registry section that tags only
foreign-phase open leases as [BLOCK]; the closing phase's own open lease
tags as [INFO]. Zero external dependencies, Node >=20 preserved.

Design rationale and source evidence are recorded as D64 in
distilled/DESIGN.md with the three required research categories cited in
distilled/EVIDENCE-INDEX.md.

Phase: 65
Adds 23 registry tests covering unit lifecycle, a cross-platform
parent-kills-child durability fixture (with .tmp orphan assertion),
corrupt-JSON quarantine, per-PID concurrent-write isolation,
subdirectory CWD resolution, closeLease state guard, and crashed_at
parity. Adds 2 new closeout-report tests confirming the registry key
is absent on fresh install and that foreign-phase open leases are
tagged [BLOCK] while the closing phase's own active lease is tagged
[INFO]. package.json test:gsdd chain extended; facade line limit
guard adjusted for the new commands.

Phase: 65
gsdd phase-status N done now refuses the status transition when
NN-PLAN-CHECK.md or NN-VERIFICATION.md is missing under the phase
folder, or when lessons-learned.md has not been touched within the
last 7 days. The gate is internal-governance scoped: it skips when
no phase folder exists and skips when .internal-research/ is absent,
so consumer projects and roadmap-only entries are unaffected.

--force --reason <text> overrides the gate and auto-appends an LL-*
entry to lessons-learned recording the bypass. --force without
--reason is refused. 8 new tests cover each refusal path, the
consumer-project skip cases, and the override path.

Phase: 65
D64 now cites verifiable sources from each required research class.

Spec-framework: GSD confirmed negative by direct inspection of 11
archived gsd-*.md role files; OpenSpec ships only a discovery YAML;
LeanSpec has no coordination state.

Orchestrator: OpenHands stores session and event state as per-event
JSON files at the persistence-dir tree (commit cae76e54, files
filesystem_event_service.py and event_service_base.py) — SQLite
appears only in the enterprise tier for billing and OAuth, not for
session state. MetaGPT writes team.json via write_json_file in team.py.
Conductor OSS uses Redis with JSON wire format in
RedisExecutionDAO.java; SqliteSchedulerDAO is scheduler-only and still
serialises JSON blobs.

Industry: Anthropic Claude Code session transcripts are append-only
JSONL; the global .claude.json has a documented race condition under
concurrent writes with 8+ GitHub issues converging on tmp+rename as
the fix — the strongest direct industry endorsement of the chosen
pattern. Cursor 2.0 ships .cursor/worktrees.json plus per-task JSON
claim files plus atomic mkdir locking — structurally identical to the
chosen design. OpenAI Codex CLI uses SQLite for resumable runtime
state and is recorded as the upgrade target for concurrent multi-
writer scenarios. GitHub Copilot Coding Agent delegates to git
worktrees with no client-side registry.

D64 title no longer embeds a phase identifier; the design entry
captures the architectural decision rather than its first consumer.
Universal claims about harness tooling are removed in favor of the
scoped, source-cited statements above.

Phase: 65
@PatrickSys PatrickSys changed the title feat(p65): worktree coordination registry — PARALLEL-01 feat: worktree coordination registry — PARALLEL-01 May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant