Skip to content

fix: read Git's absolute git dir so a Windows worktree is not the base tree - #45

Merged
hkwuks merged 5 commits into
mainfrom
fix/windows-git-path
Sep 21, 2026
Merged

hkwuks merged 5 commits into
mainfrom
fix/windows-git-path

Conversation

@hkwuks

@hkwuks hkwuks commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Summary

Adds DeepSeek Harness (dsh) as a fourth host. The repository root becomes a dsh plugin package, so a dsh user installs the skill and the SessionStart hook with one command and no hand-written profile YAML:

dsh plugin --profile web add dsh-taskflow

package.json declares dsh.bundle.patch — the marker dsh reads — and dsh/index.js mounts dsh's own @deepseek-ai/dsh-skill-filesystem over this package's skills/ and @deepseek-ai/dsh-hooks-claude-code over the new hooks/hooks-dsh.json. Both are dsh's packages rather than copies: duplicating the skill provider would duplicate frontmatter parsing, watching and catalog caching; duplicating the bridge would duplicate the matcher and the output codec.

Three measured facts shape the wiring. Each was verified against an installed dsh 0.1.5-rc.2 rather than taken from documentation, and each is the reason for a choice that would otherwise look arbitrary:

  1. A patch's baseUrl is the profile directory, not the plugin's. No bundle patch can name the files that ship beside it — so dsh/cordis.patch.yml is a single insert with no config, and every path lives in dsh/index.js, derived from import.meta.url.
  2. The bridge substitutes ${CLAUDE_PLUGIN_ROOT} inside the command string but never exports it. hooks/session-start picks its output shape from the environment, so without the CLAUDE_PLUGIN_ROOT= prefix the hook takes its fallback branch, emits a top-level additionalContext, and the bridge's codec discards it silently — exit 0, valid JSON, no context. That prefix is the entire reason hooks-dsh.json is a separate file instead of reusing hooks/hooks.json.
  3. dsh's SessionStartSource is startup | resume | clear | compact. There is no fork, so the dsh matcher omits it.

No hook script changes. hooks/session-start is byte-identical; the dsh wiring adapts to the scripts, not the reverse. The other three hosts' wiring files are untouched.

TaskFlow traceability

  • Task: TaskFlowDocs/2026-09-19-dsh-host/
  • Scope: root package.json; dsh/index.js; dsh/cordis.patch.yml; hooks/hooks-dsh.json; host lists in README.md, README.zh-CN.md, hooks/README.md, skills/taskflow/references/runtime.md, ROADMAP.md; the release-literal set in RELEASE.md, hooks/release-check, CHANGELOG.md, hooks/smoke-test. Approved Plan: TaskFlowDocs/2026-09-19-dsh-host/plan.md (v1, approved 2026-09-19 19:29 +08:00).
  • Base branch: main
  • Target repository: hkwuks/TaskFlow

Verification

  • bash hooks/smoke-testALL SMOKE PASSED
  • git diff --check → clean
  • Relevant Skill/plugin validation → see below
  • Results recorded in the TaskFlow Plan

Composition layer. dsh --profile <p> --dump-config → exit 0, with - id: taskflow / name: dsh-taskflow composited over the base and web-app layers. Verified through the literal documented command dsh plugin --profile <p> add <repo-root>, not a hand-edited profile.

Mount layer. Booting that profile with an independent observer plugin read ctx.skills.list() and got exactly ["taskflow|taskflow|custom"] — one entry, from the provider this plugin registers.

Seam layer. Driving dsh's own libraries over the hook this repository actually ships:

  • matchesMatcher accepts startup, resume, clear, compact and rejects fork, end, and the empty string.
  • parseHookOutput reads the hook's real stdout back as 397 characters of SessionStart context.
  • Two negative controls: the same stdout read as PreToolUse yields nothing (proving the hookEventName guard admitted it), and dropping the CLAUDE_PLUGIN_ROOT prefix loses the context (proving that prefix is load-bearing, not decorative).

Repository checks. bash hooks/release-check .STATUS: pass with the fourth manifest in the compared set. bash hooks/repository-check .needs-user-input, naming this task's uncommitted directory at the time it ran — expected mid-task, and its exit code is unchanged by that report.

Mutation check on the new coverage. The smoke-test section added here was verified to fail on both mutations it exists to catch: removing the CLAUDE_PLUGIN_ROOT prefix, and pointing the command at a root with no hooks.

No API call was made; the approved scope excluded a live model run.

Review boundaries

  • No secrets or opaque remote payloads included
  • No unrelated task or user files changed
  • Remote/base assumptions are stated
  • Known limitations and follow-ups are documented

Base and remote. origin is https://github.com/hkwuks/TaskFlow.git, branch feature/dsh-host, cut from main at 7dcff62 with no divergence at cut time. main was not merged into this branch during the work.

Two files in this PR are not part of the dsh change, and both are worth a look rather than a rubber stamp:

  • TaskFlowDocs/repository-docs/index.md — the SessionStart hook regenerating its own metadata dates. The identical diff exists independently in the base checkout, byte-for-byte; it is not a manual edit and not divergent.
  • TaskFlowDocs/todo.md — this task's own intake entry, TF-20260919-55110d. Another session added a different entry (TF-20260919-d6005c, on personal-rules layout) to todo.md in the base checkout, which is not in this branch. Both are appends at the same anchor, so the merge driver handles the merge — but if that other entry is still uncommitted at merge time, it needs landing separately.

Known limitations

  • No live model turn. A failure that only appears at the model seam — a malformed additionalContext, a hook that times out — is out of reach of the verification above; the seam test drives dsh's real parser to narrow the gap, not to close it.
  • Windows is untested for this host. dsh's own Windows story is unverified here, and hooks/hooks-dsh.json names no commandWindows equivalent.
  • ctx.loader.import is not a documented plugin-authoring surface. It is how dsh's own loadProfileDirectory mounts nested entries, so it is not private to the launcher, but a future dsh release could change it.
  • The npm name dsh-taskflow is unclaimed and the package could be published later, but nothing in this PR publishes, tags, or pins anything.

…e tree

Git for Windows answers `rev-parse --git-dir` with a drive path
(`D:/repo/.git/worktrees/x`). That is absolute there but starts with no
slash, so `worktree_kind` read it as relative, prefixed the root, and never
found `commondir` — every task worktree was reported as the base working
tree. `promote` refused to run and `intake` warned about a tree it had not
touched. Reading `--absolute-git-dir` instead gives a path that is usable on
every host, so the prefixing logic goes away entirely.

Verified on Linux by injecting the Windows shape: `D:/` is a legal relative
path there, so a fixture under `D:/repo/...` plus a git shim that answers with
a drive path reproduces the old failure. Reverting this change makes the new
smoke assertion fail.
…ree in the Windows suite

`D:/` is a relative path on Linux — a directory literally named `D:` — so a
fixture under `D:/repo/.git/worktrees/<name>/` plus a `git` shim that answers
`--git-dir` and `--absolute-git-dir` with a drive path reproduces the Windows
failure without a Windows host. The shim has to answer both flags: forwarding
only `--absolute-git-dir` lets the old implementation reach the real git and
fail for an unrelated reason, which is a test that passes while proving
nothing.

Adds the real-host counterpart too: the PowerShell suite now initializes a
repository, adds a linked worktree, and asserts that `promote` goes through
there while the base tree is still refused — a fix that answered "task
worktree" everywhere would pass the first half alone. Its Todo lookup now
finds the entry by goal instead of assuming the first `- ID:` line is the one
just written.
…daily

The index renderer stamped today's date into every row and into the header on
each SessionStart, so "last checked" only ever said "a session started today".
Nothing read it: the carry-over scan discarded the column, `repository-check`
only tests that the index exists, and no test asserted on it. What it did do
was rewrite a tracked file on the first session of every calendar day, which
is the uncommitted-but-tracked state that makes the next checkout refuse to
move. The renderer's own `cmp -s` guard could not help because the date was
part of the content it compared.

The table keeps Class, Source path, Phases, Exists, and Status. Carry-over
still reads rows written before the drop — they are one cell longer and the
status is still last — and now requires that last cell to look like a status,
so a date can never be mistaken for one in a row that lost its status cell.
The Windows worktree fix and the index date-column removal landed in this
branch, so the task documents that authorized them land with it: the PRD for
the confirmed facts and scope, and the Plan with the user's approval and the
verification record, including the mutation runs and the honest gap — the
PowerShell regression cannot be run on this host.
@hkwuks
hkwuks merged commit b08ce49 into main Sep 21, 2026
6 checks passed
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