feat: per-change-block hunk actions (#50), codex lanes without a pipeline, macOS LaunchAgent

- git (#50): stage-hunk/discard-hunk take an optional `block` — one run of
  consecutive +/- lines inside an @@ hunk. `onlyBlock` (server/lib/git-diff.js)
  cuts git's fresh hunk down to it, folding the other blocks into the side
  being checked; LaneDiffView hangs one review bar per block (unified + split)
  and its arrows walk blocks.
- git exclude: entries AgentHub writes for a lane go to the COMMON git dir with
  the lane's path prefix (server/lib/git-exclude.js, used by router-apply and
  lane-mcp) — a linked worktree's own info/exclude is never read by git, and a
  subdir lane has no .git. MCP sync no longer fails with ENOTDIR on a worktree
  lane or creates a stray .git in a non-repo lane.
- codex lanes carry no pipeline: codex-ingest mirrors only idle/running,
  codex-nudge is removed, agents install writes nothing for codex, the
  Workspace hides stage/pipeline/feature pickers/PipelineMap for them, and
  every server start clears leftover codex stage data and the old AGENTS.md
  stage block.
- Changes panel: a whole file opens for reading and enters the editor on a
  click on its text (caret at the clicked line), not automatically.
- macOS: LaunchAgent start-at-login (scripts/launchd-service.js, picked by
  scripts/service-manager.js).
- Adds the AgentHub introduction slides and their image-embedding script.
This commit is contained in:
2026-09-25 17:30:28 +07:00
parent 3f6e903c22
commit 952e8584f0
48 changed files with 3336 additions and 1579 deletions
+31 -11
View File
@@ -410,13 +410,7 @@ function reservedPorts(excludeLaneId = null) {
* of where the agent now says it is has not been passed for this pass; it has
* to be re-earned. Records under a key matching no node in the pipeline are
* left untouched — their position is unknown, so they cannot be proven ahead.
*
* `keepDetection` is the ONE exception to that clearing, and exists for exactly
* one caller: codex-ingest.js, which advances a codex lane's stage FROM its own
* detection. There the detection is not stale evidence of something the
* declaration superseded — it is the reason for the declaration, and the card
* has to keep saying so, or an inferred stage becomes indistinguishable from
* one the agent actually claimed.
*/
/**
* Append one row to the stage log — the history `lanes.stages` cannot keep,
@@ -472,7 +466,7 @@ function appendStageEvent(lane, nodeId, { source, result, evidence } = {}) {
}
}
function setStage(id, { stage, status, evidence, note, result, keepDetection } = {}) {
function setStage(id, { stage, status, evidence, note, result } = {}) {
const lane = getLane(id);
if (!lane) throw Object.assign(new Error(`no lane ${id}`), { code: "ENOLANE" });
const next = stage || lane.stage;
@@ -495,9 +489,7 @@ function setStage(id, { stage, status, evidence, note, result, keepDetection } =
};
db.prepare(
`UPDATE lanes SET stage = ?, stage_since = ?, status = ?, stages = ?, notes = ?, updated_at = ?${
changed && !keepDetection
? ", detected_stage = NULL, detected_signal = NULL, detected_at = NULL"
: ""
changed ? ", detected_stage = NULL, detected_signal = NULL, detected_at = NULL" : ""
}
WHERE id = ?`
).run(
@@ -864,6 +856,33 @@ function purgeLaneSessions(laneId) {
return result;
}
/**
* Codex lanes carry no pipeline, but older builds inferred and advanced one for
* them. Wipe what those builds left — the stage columns, the stage log and any
* feature records — so nothing stale survives into the Workflows statistics or
* resurfaces if a view ever reads it. Idempotent; run at every server start.
*
* @returns {number} how many codex lanes still carried pipeline data
*/
function clearCodexPipelines() {
const CODEX = "SELECT id FROM lanes WHERE tool = 'codex'";
let changed = 0;
db.transaction(() => {
changed = db
.prepare(
`UPDATE lanes SET stage = 'idle', stage_since = NULL, stages = '{}',
detected_stage = NULL, detected_signal = NULL, detected_at = NULL
WHERE tool = 'codex' AND (stage <> 'idle' OR stages <> '{}' OR detected_stage IS NOT NULL
OR id IN (SELECT lane_id FROM lane_stage_events)
OR id IN (SELECT lane_id FROM lane_features))`
)
.run().changes;
db.prepare(`DELETE FROM lane_stage_events WHERE lane_id IN (${CODEX})`).run();
db.prepare(`DELETE FROM lane_features WHERE lane_id IN (${CODEX})`).run();
})();
return changed;
}
module.exports = {
DEAD_SEC,
VALID_TOOLS,
@@ -875,6 +894,7 @@ module.exports = {
updateLane,
deleteLane,
setStage,
clearCodexPipelines,
setRouterProfile,
lanesByRouter,
recordDetection,