Skip to content

chore: decommission legacy bash install flow#3

Merged
firatcand merged 1 commit into
mainfrom
chore/decommission-bash-install-flow
May 8, 2026
Merged

chore: decommission legacy bash install flow#3
firatcand merged 1 commit into
mainfrom
chore/decommission-bash-install-flow

Conversation

@firatcand
Copy link
Copy Markdown
Owner

Summary

Removes the original bash-based install path. npm publishing (npx @firatcand/forge) is now the canonical adopter route — dual install paths were drift waiting to happen.

Deleted:

  • setup.sh (87 lines): symlinked skills/agents into ~/.claude and the bash forge launcher into ~/.local/bin/forge. Both jobs are now done by npx @firatcand/forge install.
  • forge (168 lines): bash CLI with forge init, forge templates list, forge templates copy, forge upgrade, forge version. All replaced by bin/forge.js subcommands shipped via npm in v0.2.x.

Updated:

  • README.md: drop the "Prefer the original bash flow?" fallback line below the install section.
  • CONTRIBUTING.md: bug-fix scope now points at bin/forge.js + lib/tools.js; local validation block updated from bash -n setup.sh forge lib/*.sh to node --check bin/forge.js lib/*.js (lib's .sh helpers stay and still get bash -n'd).

Untouched on purpose:

  • lib/*.sh (github-helpers, linear-helpers, validators, worktree-helpers) — these are sourced from skill bodies and templates, not from the deleted bash launcher. Out of scope for this PR.
  • CHANGELOG.md v1.0.0 entry mentioning setup.sh — historical record, immutable.
  • Version stays at 0.2.1. No published-API change: setup.sh/forge were never in package.json:files, so the npm tarball is byte-identical.

Why now

The parent investigation that produced PR #1 (forge doctor) traced its root cause back to silent drift between forge's stated cross-tool support and what actually worked. Carrying two install paths — npm-shipped and bash-shipped — multiplies that same risk. One canonical path is cheaper to reason about and harder to lie about in docs.

Test plan

  • node --check bin/forge.js lib/*.js passes
  • bash -n lib/*.sh passes
  • node bin/forge.js --version0.2.1
  • node bin/forge.js doctor runs cleanly
  • Manual verify after merge: nothing in skill bodies or templates references ./setup.sh or the bash ./forge (will spot-check once landed)

🤖 Generated with Claude Code

Removes the original bash-based install path now that npm publishing is
the canonical adopter route:
- delete `setup.sh` (87 lines): symlinked skills/agents into ~/.claude
  and the bash forge launcher into ~/.local/bin/forge — both jobs are
  now done by `npx @firatcand/forge install`
- delete `forge` (168 lines): bash CLI with `forge init`, `forge
  templates`, `forge upgrade`, `forge version` — all replaced by
  bin/forge.js subcommands shipped via npm
- README: drop the "Prefer the original bash flow?" fallback line
- CONTRIBUTING: update bug-fix scope to `bin/forge.js` + `lib/tools.js`,
  replace `bash -n setup.sh forge lib/*.sh` validation with
  `node --check bin/forge.js lib/*.js` plus `bash -n lib/*.sh` (the lib
  helpers stay)

Why: dual install paths (bash + npm) are exactly the kind of drift that
caused this PR's parent investigation. Consolidating to a single
canonical flow removes a documented-but-stale fallback. CHANGELOG entry
for v1.0.0 deliberately left intact as historical record.

Local impact: the orphaned `~/.local/bin/forge` symlink that was created
by setup.sh on this machine has already been removed manually. Future
contributors who clone fresh use `npm install` then `node bin/forge.js`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@firatcand firatcand merged commit 37b76c1 into main May 8, 2026
@firatcand firatcand deleted the chore/decommission-bash-install-flow branch May 8, 2026 12:13
firatcand added a commit that referenced this pull request May 14, 2026
… (Codex review) (#95)

Closes 4 implementation bugs and 2 cheap enhancements that an earlier Codex
review of FORGE-64 (PR #87) surfaced in the question/answer atomic writer.
Architectural choice (link+unlink over rename for OS-level enforcement of
the "never overwritten" invariant) is unchanged.

- Bug #1: try/finally ensures temp-file cleanup on every code path including
  DUPLICATE_ID. Cleanup ownership moved from inside placeAtomic to the
  caller's finally block — single unconditional unlink point.
- Bug #2: writeSync loop with Buffer offsets. Short writes are recoverable
  (NFS / quota / signal-interrupted); writeSync returning 0 with bytes still
  to write is treated as IO_ERROR rather than an infinite-loop trigger.
- Bug #3: closeSync errors surface as IO_ERROR when no prior write/fsync
  error exists (delayed write-back on NFS/quota paths per close(2)). Prior
  errors win — close error is informational, prior error is actionable.
- Bug #4: payload byte-size cap enforced at the write boundary
  (PAYLOAD_TOO_LARGE) before any disk I/O. Schema caps strings by UTF-16
  code units; the cap is enforced in UTF-8 bytes. A schema-valid payload of
  4-byte-per-codepoint characters can exceed 64KB even while passing zod
  validation — the cap closes that gap.
- Enh #6: temp filenames use crypto.randomBytes(8).toString('hex') instead
  of Math.random(). Closes the hostile-same-UID-process question definitively.
- Enh #7: durability contract documented in spec/ORCHESTRATOR.md "File
  semantics" — file fsync protects against partial writes, not power-loss
  durability of placement; dispatcher reconciles from tracker on restart.
- Enh #5: deferred to plans/BACKLOG.md (capability probe → forge doctor).

Internal: adds __fsForTesting export on src/orchestrator/questions/writer.ts
as a test seam. node:test's mock.method cannot patch the frozen node:fs
Module Namespace directly; the seam is a plain mutable object holding fs
method references used internally by the writer. Tests use mock.method on
this object to simulate partial writes, closeSync failures, and validate
the random-bytes temp-name shape. Not part of the public API.

Test coverage: +8 unit tests covering each bug and Enh #6. Total 16 tests
in writer.test.ts (was 8), all pass. Full suite 609 pass / 0 fail / 8
pre-existing skipped. Build OK, smoke ./dist/bin/forge.cjs --version OK.

Codex second-opinion review on the diff: no findings.

Unblocks FORGE-20 (dispatcher core).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
firatcand added a commit that referenced this pull request May 17, 2026
…RGE-113)

Step 5 of plans/tasks/FORGE-113.plan.md.

src/cli/orchestrate/reconcile.ts:
- loadPhasesWithDocument now returns {phases, doc, raw, freshnessLine};
  caller prints freshnessLine to stderr immediately after a successful
  load, before any further verb work.
- runPull resolves a Source object and installs it on the YAML
  Document via setIn(['source', ...]) before writeAtomic. Existing
  doc-level mutations from applyPlanToDocument are preserved.
- runPull now writes phases.yaml UNCONDITIONALLY on a successful --pull
  (not gated on mutationsDoc > 0). Rationale: synced_at means "last
  successful sync attempt", not "last mutation". A zero-diff --pull is
  still a successful sync and should bump the timestamp. `applied` in
  the JSON envelope continues to reflect plan mutations only (so
  consumers checking "did issues change?" still get the right answer).

Source resolution prefers, in order:
  1. The existing `source.project_id` (preserved across --pull runs)
  2. The legacy top-level `tracker_project_id` (migration path; the
     schema strips this on parse but the raw Document still has it
     until the first --pull rewrites the file).
Throws SOURCE_RESOLUTION_FAILED if neither is found — fail loudly
rather than fabricate a project_id.

setSourceOnDocument also deletes the legacy top-level
tracker_project_id key from the Document — idempotent migration runs
on every --pull until adopters are fully on the v0.4 shape.

Tests:
- MINIMAL_PHASES fixture extended with a `source:` block so all
  existing tests survive the new resolution requirement.
- mkScratchWorktree now seeds spec/SPEC.md (computeSpecRevision needs
  it; falls back to content digest in the absence of git).
- stripFreshness() helper for tests that JSON.parse stderr — the
  freshness line is prepended ahead of any structured envelope.
- New unit tests:
  · "--pull writes source stanza atomically" (AC #3)
  · "--pull migrates legacy tracker_project_id into source" (AC #2)
  · "freshness line printed to stderr before main output"
- E2E PHASES_FIXTURE updated identically; scratch dir gets SPEC.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
firatcand added a commit that referenced this pull request May 17, 2026
…eshness display (P2.5-T17) (#171)

* chore(phases): amend FORGE-113 AC + migrate top-level tracker_project_id

Pre-implementation migration (Step 1 of plans/tasks/FORGE-113.plan.md):

- plans/phases.yaml P2.5-T17 entry: drop tracker_revision from schema +
  AC; add explicit AC for the breaking removal of top-level
  tracker_project_id; add AC for missing-source freshness line; update
  freshness format example.
- This repo's plans/phases.yaml: move line-2 tracker_project_id into a
  new top-level `source:` block (tracker, project_id, synced_at,
  spec_revision). Schema still accepts the old shape (tracker_project_id
  was optional; source is currently unknown-key-stripped). Pre-stages
  the file for the schema change in Step 2.

Decision: tracker_revision dropped → live-drift detection deferred to
follow-up issue (filed in Step 10) per user answer after Codex 2nd-pass.

Decision: source.project_id replaces top-level tracker_project_id
(breaking) per user answer; tracker_url stays at top level per user
answer on plan review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schemas): add Source + remove tracker_project_id from PhasesSchema (FORGE-113)

Step 2 of plans/tasks/FORGE-113.plan.md.

- Introduce TRACKER_TYPES enum + SourceSchema (zod, strict):
  { tracker, project_id, synced_at: ISO-8601, spec_revision }
- Attach `source: SourceSchema.optional()` to PhasesSchema
- Remove `tracker_project_id` from PhasesSchema top level (breaking)
- Export `Source` and `TrackerType` types

Tests:
- Replace existing tracker_project_id+tracker_url combined test with a
  tracker_url-only test
- Add coverage for: optional source-block round-trip, rejected unknown
  tracker enum, rejected non-ISO synced_at, rejected extra keys inside
  source (strict mode catches tracker_revision and other unintended
  fields)

Decision: SourceSchema is `.strict()` so the deliberately-omitted
`tracker_revision` field cannot be silently smuggled back in by a
hand-edit or stale tool. Future addition (FORGE-XXX follow-up for
live-drift detection) will require an explicit schema change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(core): add freshness + spec-revision helpers (FORGE-113)

Step 3 of plans/tasks/FORGE-113.plan.md. Pure helpers behind the
freshness summary printed by every CLI verb that reads phases.yaml.

src/core/freshness.ts:
- formatRelativeAge(ms): "just now" | "Nmin" | "Nh" | "Nd"
- computeFreshnessLine(source, now): two output shapes
  · `phases.yaml: synced 47min ago from linear (SPEC@8fa2226)`
  · `phases.yaml: ⚠ STALE — synced 1d ago from linear (SPEC@8fa2226)`
    when age > 24h, OR the documented fallback when source is absent.
- 24h stale threshold is exclusive (24h exactly is not stale).
- Defensive branch for unparseable synced_at past the zod boundary.

src/core/spec-revision.ts:
- computeSpecRevision(cwd): git sha of HEAD when spec/SPEC.md was last
  touched, falling back to a 40-char sha256 content digest when the
  file is untracked or the directory isn't a git working tree.
- Throws SpecRevisionError if spec/SPEC.md doesn't exist — that's a
  misconfiguration, not a defensive fallback.
- Uses execFileSync (not exec) for the git invocation; arg array
  prevents shell injection paths from a hostile cwd.

Tests cover: relative-age boundaries, 24h stale boundary on both sides,
missing-source fallback string, unparseable defense, all 3 tracker
types verbatim, short spec_revision slicing; git-sha for tracked,
content-digest for untracked + non-git, last-commit-touching-SPEC.md
semantics (HEAD advancing without touching SPEC.md doesn't change the
rev), throw on missing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(core,cli): loadPhases returns {phases, freshnessLine} (FORGE-113)

Step 4 of plans/tasks/FORGE-113.plan.md.

Loader contract change (decision: FORGE-113 plan §0 Q1):
- `loadPhases(path): Phases` → `loadPhases(path): LoadPhasesResult`
  where LoadPhasesResult = { phases, freshnessLine }.
- freshnessLine is computed at parse time via computeFreshnessLine
  against the optional `source` block. Loader stays pure of stderr
  I/O; each caller decides where (or whether) to surface the line.
- Failures (FILE_NOT_FOUND, PARSE_ERROR, SCHEMA_INVALID) still throw
  PhasesError — the new return shape applies only to the success path.

Callers updated to plumb freshnessLine to stderr before main output:
- src/cli/orchestrate/phases.ts:87
- src/bin/sync-status-render.ts:31
(The third reader, reconcile's loadPhasesWithDocument, is handled in
Step 6 alongside its --pull write path.)

Tests:
- test/unit/core/phases.test.ts updated to the new return shape on the
  two success-path assertions; new "result includes a freshnessLine
  string" coverage. Error-path tests unchanged — throw semantics
  preserved.

Schema cleanup:
- Removed duplicate `export type TrackerType` from src/schemas/phases.ts
  (canonical alias remains in src/trackers/types.ts; barrel re-export
  was ambiguous through src/index.ts).
- src/core/index.ts re-exports LoadPhasesResult, Source,
  computeFreshnessLine, formatRelativeAge, computeSpecRevision,
  SpecRevisionError for downstream consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(reconcile): stamp source stanza + freshness stderr on --pull (FORGE-113)

Step 5 of plans/tasks/FORGE-113.plan.md.

src/cli/orchestrate/reconcile.ts:
- loadPhasesWithDocument now returns {phases, doc, raw, freshnessLine};
  caller prints freshnessLine to stderr immediately after a successful
  load, before any further verb work.
- runPull resolves a Source object and installs it on the YAML
  Document via setIn(['source', ...]) before writeAtomic. Existing
  doc-level mutations from applyPlanToDocument are preserved.
- runPull now writes phases.yaml UNCONDITIONALLY on a successful --pull
  (not gated on mutationsDoc > 0). Rationale: synced_at means "last
  successful sync attempt", not "last mutation". A zero-diff --pull is
  still a successful sync and should bump the timestamp. `applied` in
  the JSON envelope continues to reflect plan mutations only (so
  consumers checking "did issues change?" still get the right answer).

Source resolution prefers, in order:
  1. The existing `source.project_id` (preserved across --pull runs)
  2. The legacy top-level `tracker_project_id` (migration path; the
     schema strips this on parse but the raw Document still has it
     until the first --pull rewrites the file).
Throws SOURCE_RESOLUTION_FAILED if neither is found — fail loudly
rather than fabricate a project_id.

setSourceOnDocument also deletes the legacy top-level
tracker_project_id key from the Document — idempotent migration runs
on every --pull until adopters are fully on the v0.4 shape.

Tests:
- MINIMAL_PHASES fixture extended with a `source:` block so all
  existing tests survive the new resolution requirement.
- mkScratchWorktree now seeds spec/SPEC.md (computeSpecRevision needs
  it; falls back to content digest in the absence of git).
- stripFreshness() helper for tests that JSON.parse stderr — the
  freshness line is prepended ahead of any structured envelope.
- New unit tests:
  · "--pull writes source stanza atomically" (AC #3)
  · "--pull migrates legacy tracker_project_id into source" (AC #2)
  · "freshness line printed to stderr before main output"
- E2E PHASES_FIXTURE updated identically; scratch dir gets SPEC.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(integration): phases staleness e2e (FORGE-113 AC #7)

Step 7 of plans/tasks/FORGE-113.plan.md.

test/integration/cli/orchestrate/phases.staleness.test.ts (new):
  · 25h-stale file: exit 0 + stderr contains
    "phases.yaml: ⚠ STALE — synced 1d ago from linear (SPEC@<digest>)"
    Exits 0 (file is usable); staleness is surfaced via stderr, not exit
    code — matches AC #7 wording ("still works but prints the staleness
    duration prominently").
  · 23h-fresh file: exit 0, no STALE marker, "synced Nh ago" format.
    Pins the 24h boundary the unit tests already cover, exercised
    end-to-end through the verb.
  · No source block: exit 0, stderr matches documented fallback
    "phases.yaml: no source metadata (run forge orchestrate reconcile
    --pull to sync)" — confirms AC #4 in the actual CLI surface, not
    just in computeFreshnessLine unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs,templates,skills: migrate references to source block (FORGE-113)

Step 8 of plans/tasks/FORGE-113.plan.md. Aligns all surfaces that
reference the removed top-level `tracker_project_id` field with the
new `source` block.

- templates/phases.template.yaml: drop top-level `tracker_project_id`;
  add commented `source:` block scaffold so adopters see the
  post-reconcile shape on first inspection. Real values are populated
  by /push-to-tracker → /reconcile --pull.
- test/fixtures/orchestrator/phases.yaml: migrate to source block.
- skills/push-to-tracker/SKILL.md: update both occurrences — the
  tracker-agnostic-keys list and the update-mode edge-case comment.
- docs/LIFECYCLE.md: update the /push-to-tracker outputs description.
- docs/EXAMPLES.md: update the post-bootstrap line.
- docs/trackers/linear.md: update the post-push field list.

Migration is automatic for existing adopter projects: the first
/reconcile --pull transplants legacy top-level tracker_project_id into
source.project_id and deletes the legacy key. No adopter action needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(spec): align §source block + freshness format with shipped reality (FORGE-113)

Step 9 of plans/tasks/FORGE-113.plan.md. Surgical update — does NOT
refresh the broader stale §phases.yaml schema code block (filed as
follow-up).

§`phases.yaml` is a derived snapshot:
- Source block field list now matches shipped schema:
  `{tracker, project_id, synced_at, spec_revision}` (no
  tracker_revision).
- Freshness format updated to "from <tracker>" (no
  "@<tracker_revision>"): `phases.yaml: synced 47min ago from linear
  (SPEC@a3c2d1f)`.
- Added the missing-source fallback string verbatim.
- Documented the 24h "⚠ STALE — " prominence prefix and the
  "exit 0 + stderr surfacing" semantic.
- Rationale paragraph for the deliberate absence of tracker_revision:
  no v0.4 consumer; the right shape for live drift detection is
  Tracker.getCurrentRevision() (provider-native cheap rev), filed as
  a follow-up issue.
- Breaking change note: top-level tracker_project_id moved into
  source.project_id; migration is automatic on first --pull.

§`phases.yaml` schema (zod):
- Added single-line note pointing readers at src/schemas/phases.ts as
  canonical (the in-spec code block predates FORGE-96 lifecycle fields
  and FORGE-113 source block; broader refresh deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(spec): point follow-up reference at filed FORGE-123 (FORGE-113)

Step 10 of plans/tasks/FORGE-113.plan.md. Filed FORGE-123 (Add
Tracker.getCurrentRevision() for live drift detection) and updated
the SPEC.md rationale paragraph to reference the real issue ID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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