@brainervirus/workit-core 8.5.2 → 8.6.1
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/package.json +1 -1
- package/src/git/ops.ts +37 -3
- package/src/hooks/context.ts +12 -8
- package/src/hooks/hosts/claude-code.ts +2 -3
- package/src/hooks/hosts/pi.ts +4 -4
- package/src/hooks/index.ts +2 -0
- package/src/ledger.ts +6 -2
package/package.json
CHANGED
package/src/git/ops.ts
CHANGED
|
@@ -811,6 +811,32 @@ export type PushOutcome = {
|
|
|
811
811
|
};
|
|
812
812
|
|
|
813
813
|
const SHA = /^[0-9a-f]{40,64}$/u;
|
|
814
|
+
const SHA_PREFIX = /^[0-9a-f]{7,39}$/u;
|
|
815
|
+
|
|
816
|
+
/** Resolve an abbreviated `--expect` against the known remote tips; refuse unknown or ambiguous. */
|
|
817
|
+
export function resolveExpectPrefix(
|
|
818
|
+
prefix: string,
|
|
819
|
+
tips: ReadonlyArray<string | null | undefined>,
|
|
820
|
+
label: string,
|
|
821
|
+
): { ok: true; sha: string } | { ok: false; code: "invalid_input"; error: string } {
|
|
822
|
+
const known = [...new Set(tips.filter((tip): tip is string => typeof tip === "string"))];
|
|
823
|
+
if (!SHA_PREFIX.test(prefix))
|
|
824
|
+
return {
|
|
825
|
+
ok: false,
|
|
826
|
+
code: "invalid_input",
|
|
827
|
+
error: "--expect must be a commit sha (full, or an abbreviation of at least 7 hex digits)",
|
|
828
|
+
};
|
|
829
|
+
const matches = known.filter((tip) => tip.startsWith(prefix));
|
|
830
|
+
if (matches.length === 1) return { ok: true, sha: matches[0] };
|
|
831
|
+
return {
|
|
832
|
+
ok: false,
|
|
833
|
+
code: "invalid_input",
|
|
834
|
+
error:
|
|
835
|
+
matches.length > 1
|
|
836
|
+
? `--expect ${prefix} is ambiguous for ${label}; choose one of: ${matches.join(", ")}`
|
|
837
|
+
: `--expect ${prefix} matches no known tip of ${label}${known.length ? `; known: ${known.join(", ")}` : ""}; pass the full sha`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
814
840
|
|
|
815
841
|
export function executePush(
|
|
816
842
|
cwd: string,
|
|
@@ -835,10 +861,18 @@ export function executePush(
|
|
|
835
861
|
const args = ["push", "--porcelain", "--no-follow-tags", "--recurse-submodules=no"];
|
|
836
862
|
if (input.forceWithLease) {
|
|
837
863
|
const explicit = input.expect !== undefined && input.expect !== null;
|
|
838
|
-
|
|
839
|
-
|
|
864
|
+
let expectSha = input.expect ?? null;
|
|
865
|
+
if (explicit && !SHA.test(expectSha as string)) {
|
|
866
|
+
const resolved = resolveExpectPrefix(
|
|
867
|
+
expectSha as string,
|
|
868
|
+
[before.sha, recordedRemoteTip(cwd, plan)],
|
|
869
|
+
`${plan.remote}/${plan.branch}`,
|
|
870
|
+
);
|
|
871
|
+
if (!resolved.ok) return failure(resolved.code, resolved.error);
|
|
872
|
+
expectSha = resolved.sha;
|
|
873
|
+
}
|
|
840
874
|
const integrate = `git fetch ${plan.remote} ${plan.branch} && git rebase ${plan.remote}/${plan.branch} # or merge it; then: workit git push`;
|
|
841
|
-
const recorded = explicit ? (
|
|
875
|
+
const recorded = explicit ? (expectSha as string) : recordedRemoteTip(cwd, plan);
|
|
842
876
|
// A new branch needs no lease; otherwise workit must have recorded the tip.
|
|
843
877
|
if (recorded === undefined && before.sha !== null)
|
|
844
878
|
return failure(
|
package/src/hooks/context.ts
CHANGED
|
@@ -291,11 +291,11 @@ export const turnContextText = (input: HookInput, descriptor: HostDescriptor): s
|
|
|
291
291
|
}
|
|
292
292
|
};
|
|
293
293
|
|
|
294
|
-
// Change-only per-turn context.
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// session
|
|
298
|
-
// workspace store (`hooks/turn-<session hash>.json`). Every cache failure
|
|
294
|
+
// Change-only per-turn context. Claude Code, Codex and Pi keep each injected
|
|
295
|
+
// context in the transcript, so the task context is resent only when it
|
|
296
|
+
// changed since the last injection for the session. Each hook is a fresh
|
|
297
|
+
// process (and a Pi session can resume in another one): the last digest
|
|
298
|
+
// lives in the workspace store (`hooks/turn-<session hash>.json`). Every cache failure
|
|
299
299
|
// answers "changed", so context is resent rather than lost.
|
|
300
300
|
|
|
301
301
|
const TURN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -376,13 +376,17 @@ export const changedTurnContext = (input: HookInput, text: string | null): strin
|
|
|
376
376
|
* A session start already injects the task context (inside the contract), so
|
|
377
377
|
* the per-turn digest is seeded with what the next prompt would carry: the
|
|
378
378
|
* first turn after a start, resume or compaction does not resend it. A
|
|
379
|
-
* session without task context clears its digest instead.
|
|
379
|
+
* session without task context clears its digest instead. An in-process
|
|
380
|
+
* host that renders its own per-turn `text` seeds with it.
|
|
380
381
|
*/
|
|
381
|
-
export const seedTurnContext = (
|
|
382
|
+
export const seedTurnContext = (
|
|
383
|
+
input: HookInput,
|
|
384
|
+
descriptor: HostDescriptor,
|
|
385
|
+
text: string | null = turnContextText(input, descriptor),
|
|
386
|
+
): void => {
|
|
382
387
|
const file = turnMarker(input);
|
|
383
388
|
if (!file) return;
|
|
384
389
|
pruneTurnDigests(path.dirname(file), Date.now());
|
|
385
|
-
const text = turnContextText(input, descriptor);
|
|
386
390
|
if (text) writeTurnDigest(file, digestOf(text));
|
|
387
391
|
else clearTurnDigest(file);
|
|
388
392
|
};
|
|
@@ -58,9 +58,8 @@ export const CLAUDE_CODE_DESCRIPTOR: HostDescriptor = {
|
|
|
58
58
|
perTurn: "native",
|
|
59
59
|
afterCompact: "native",
|
|
60
60
|
task: "session-bound",
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
turnResend: "every-turn",
|
|
61
|
+
// Each additionalContext stays in the transcript: resend only on change.
|
|
62
|
+
turnResend: "on-change",
|
|
64
63
|
},
|
|
65
64
|
subagents: {
|
|
66
65
|
identity: "native",
|
package/src/hooks/hosts/pi.ts
CHANGED
|
@@ -24,12 +24,12 @@ export const PI_DESCRIPTOR: HostDescriptor = {
|
|
|
24
24
|
shellPolicy: { deny: "native", channel: "block", failClosed: false },
|
|
25
25
|
context: {
|
|
26
26
|
sessionStart: "native",
|
|
27
|
-
// before_agent_start runs every turn
|
|
28
|
-
//
|
|
29
|
-
perTurn: "
|
|
27
|
+
// before_agent_start runs every turn; its message stays in the session,
|
|
28
|
+
// so the extension resends the task context only when it changed.
|
|
29
|
+
perTurn: "native",
|
|
30
30
|
afterCompact: "native",
|
|
31
31
|
task: "session-bound",
|
|
32
|
-
turnResend: "
|
|
32
|
+
turnResend: "on-change",
|
|
33
33
|
},
|
|
34
34
|
subagents: {
|
|
35
35
|
identity: "none",
|
package/src/hooks/index.ts
CHANGED
package/src/ledger.ts
CHANGED
|
@@ -1608,7 +1608,11 @@ export function clearStanding(
|
|
|
1608
1608
|
* Is `branch` checked out, with uncommitted changes, in another worktree of
|
|
1609
1609
|
* this repository? (`codeKey` sees only the cwd's own worktree.)
|
|
1610
1610
|
*/
|
|
1611
|
-
function dirtyElsewhere(cwd: string, branch: string): boolean {
|
|
1611
|
+
function dirtyElsewhere(cwd: string, branch: string, head: string): boolean {
|
|
1612
|
+
// A checkout of exactly the branch head (a verifier's detached worktree)
|
|
1613
|
+
// judges what it holds: clean judges that head, whatever state another
|
|
1614
|
+
// checkout of the branch is in; dirty (or unreadable) does not.
|
|
1615
|
+
if (headSha(cwd) === head) return worktreeTree(cwd)?.dirty !== false;
|
|
1612
1616
|
const list = git(cwd, ["worktree", "list", "--porcelain"]);
|
|
1613
1617
|
let worktree: string | null = null;
|
|
1614
1618
|
for (const line of (list ?? "").split("\n")) {
|
|
@@ -1668,7 +1672,7 @@ export function recordVerdict(
|
|
|
1668
1672
|
if (!ledger.ok) return ledger;
|
|
1669
1673
|
const key = codeKey(context.cwd, { branch, base: context.base });
|
|
1670
1674
|
if (!key.head) return err("not_found", `branch "${branch}" has no commit to judge`);
|
|
1671
|
-
if (key.dirty === true || (key.dirty === null && dirtyElsewhere(context.cwd, branch)))
|
|
1675
|
+
if (key.dirty === true || (key.dirty === null && dirtyElsewhere(context.cwd, branch, key.head)))
|
|
1672
1676
|
return err(
|
|
1673
1677
|
"blocked",
|
|
1674
1678
|
`dirty_worktree: ${branch} has uncommitted changes, so its head is not what was judged`,
|