@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The no-op guard: does the build half actually produce a change?
|
|
3
|
+
*
|
|
4
|
+
* The pipeline's review half is diff-driven — each reviewer runs `git diff`
|
|
5
|
+
* itself. So an implement stage that exits 0 having changed NOTHING (a cheap
|
|
6
|
+
* model narrating a "## Files changed" summary without calling write/edit, an
|
|
7
|
+
* empty plan handoff, etc.) leaves an empty tree, the reviewers correctly find
|
|
8
|
+
* nothing, and the run would otherwise report `clean` — a false "reviewed and
|
|
9
|
+
* clean" for work that never happened. This module gives the orchestrator a
|
|
10
|
+
* cheap, honest signal to stop with `no_changes` instead.
|
|
11
|
+
*
|
|
12
|
+
* `workspaceChanged` is PURE (unit-tested directly). `snapshotWorkspace` is the
|
|
13
|
+
* thin, injectable git seam — it shells out like `reviewTool.ts#runGit` because
|
|
14
|
+
* pi's ExtensionContext exposes no exec. It is resilient: any git error (no repo,
|
|
15
|
+
* git missing) yields `tracked: false`, and the pure comparator then FAILS OPEN
|
|
16
|
+
* (assumes a change) rather than mis-firing `no_changes`.
|
|
17
|
+
*/
|
|
18
|
+
import type { WorkspaceSnapshot } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Did the working tree change between two snapshots? A different HEAD means a
|
|
21
|
+
* stage committed; a different porcelain status means an uncommitted edit. When
|
|
22
|
+
* EITHER snapshot is untracked we cannot assess the tree, so we fail open and
|
|
23
|
+
* report `true` — the guard only stops a run it is certain produced nothing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function workspaceChanged(before: WorkspaceSnapshot, after: WorkspaceSnapshot): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Capture the repo's HEAD + porcelain status at `cwd`. `git rev-parse HEAD`
|
|
28
|
+
* failing (empty result) means the cwd is not a usable git work tree, so we
|
|
29
|
+
* report `tracked: false` and skip the second call.
|
|
30
|
+
*/
|
|
31
|
+
export declare function snapshotWorkspace(cwd: string, signal?: AbortSignal): Promise<WorkspaceSnapshot>;
|
|
32
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The no-op guard: does the build half actually produce a change?
|
|
3
|
+
*
|
|
4
|
+
* The pipeline's review half is diff-driven — each reviewer runs `git diff`
|
|
5
|
+
* itself. So an implement stage that exits 0 having changed NOTHING (a cheap
|
|
6
|
+
* model narrating a "## Files changed" summary without calling write/edit, an
|
|
7
|
+
* empty plan handoff, etc.) leaves an empty tree, the reviewers correctly find
|
|
8
|
+
* nothing, and the run would otherwise report `clean` — a false "reviewed and
|
|
9
|
+
* clean" for work that never happened. This module gives the orchestrator a
|
|
10
|
+
* cheap, honest signal to stop with `no_changes` instead.
|
|
11
|
+
*
|
|
12
|
+
* `workspaceChanged` is PURE (unit-tested directly). `snapshotWorkspace` is the
|
|
13
|
+
* thin, injectable git seam — it shells out like `reviewTool.ts#runGit` because
|
|
14
|
+
* pi's ExtensionContext exposes no exec. It is resilient: any git error (no repo,
|
|
15
|
+
* git missing) yields `tracked: false`, and the pure comparator then FAILS OPEN
|
|
16
|
+
* (assumes a change) rather than mis-firing `no_changes`.
|
|
17
|
+
*/
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
|
+
/**
|
|
20
|
+
* Did the working tree change between two snapshots? A different HEAD means a
|
|
21
|
+
* stage committed; a different porcelain status means an uncommitted edit. When
|
|
22
|
+
* EITHER snapshot is untracked we cannot assess the tree, so we fail open and
|
|
23
|
+
* report `true` — the guard only stops a run it is certain produced nothing.
|
|
24
|
+
*/
|
|
25
|
+
export function workspaceChanged(before, after) {
|
|
26
|
+
if (!before.tracked || !after.tracked)
|
|
27
|
+
return true;
|
|
28
|
+
return after.head !== before.head || after.status !== before.status;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Run one git command, resolving to trimmed stdout or "" on any error.
|
|
32
|
+
*
|
|
33
|
+
* `signal` is deliberately NOT passed into execFile: Node's built-in abort
|
|
34
|
+
* handling kills via the child's pid, and when the spawn itself failed (missing
|
|
35
|
+
* cwd, missing git) an abort landing in the same tick resolves that to pid 0 —
|
|
36
|
+
* signalling the WHOLE process group and killing the parent (observed as the
|
|
37
|
+
* test runner dying with 128+SIGTERM). Abort is wired manually with an explicit
|
|
38
|
+
* pid guard instead; a killed child still reaches the callback, which resolves
|
|
39
|
+
* "" as for any other error.
|
|
40
|
+
*/
|
|
41
|
+
function runGit(args, cwd, signal) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
if (signal?.aborted)
|
|
44
|
+
return resolve("");
|
|
45
|
+
const child = execFile("git", args, { cwd, maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
|
|
46
|
+
signal?.removeEventListener("abort", onAbort);
|
|
47
|
+
resolve(err ? "" : stdout.toString().trim());
|
|
48
|
+
});
|
|
49
|
+
const onAbort = () => {
|
|
50
|
+
try {
|
|
51
|
+
if (child.pid)
|
|
52
|
+
child.kill("SIGTERM");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* ignore */
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Capture the repo's HEAD + porcelain status at `cwd`. `git rev-parse HEAD`
|
|
63
|
+
* failing (empty result) means the cwd is not a usable git work tree, so we
|
|
64
|
+
* report `tracked: false` and skip the second call.
|
|
65
|
+
*/
|
|
66
|
+
export async function snapshotWorkspace(cwd, signal) {
|
|
67
|
+
const head = await runGit(["rev-parse", "HEAD"], cwd, signal);
|
|
68
|
+
if (!head)
|
|
69
|
+
return { tracked: false, head: "", status: "" };
|
|
70
|
+
const status = await runGit(["status", "--porcelain"], cwd, signal);
|
|
71
|
+
return { tracked: true, head, status };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-worktree plumbing for worktree-by-default /go (spec §3b).
|
|
3
|
+
*
|
|
4
|
+
* A /go run gets its own detached worktree + run branch BEFORE anything else
|
|
5
|
+
* (`git worktree add -b yagni/<slug>-<shortId> ~/.yagni-code/worktrees/<shortId>
|
|
6
|
+
* HEAD`), so repoCtx, sessionKey, and every snapshot are resolved against the
|
|
7
|
+
* worktree path and the run can never fold the user's live tree into its diff.
|
|
8
|
+
*
|
|
9
|
+
* Three impure seams, each with the pure decision parts unit-testable:
|
|
10
|
+
* - {@link createRunWorktree} — the `git worktree add` shell-out (throws with
|
|
11
|
+
* git's stderr so the caller can fall back to in-place honestly).
|
|
12
|
+
* - {@link bootstrapWorktree} — best-effort `<pm> install --prefer-offline`
|
|
13
|
+
* with an 8-minute cap, because a fresh worktree has no node_modules and
|
|
14
|
+
* without it every verify/typecheck exits `command not found`. Fail-OPEN
|
|
15
|
+
* with an honest note; an opt-in `.yagni-code/bootstrap` file (same trust
|
|
16
|
+
* model as `.yagni-code/verify`: only read when the operator set
|
|
17
|
+
* `YAGNI_CODE_ALLOW_VERIFY_OVERRIDE=1`) overrides the detected install. The
|
|
18
|
+
* install runs under {@link buildVerifyEnv} so repo-triggered commands never
|
|
19
|
+
* inherit YAGNI/AI tokens (invariant §0.9).
|
|
20
|
+
* - {@link commitWip} — the WIP commit on a NON-clean terminal outcome
|
|
21
|
+
* (`wip(yagni): <ticket> - <stopReason>`), clearly marked and worktree-local,
|
|
22
|
+
* so round_cap/partial work is never lost and /go-clean can remove the tree.
|
|
23
|
+
*
|
|
24
|
+
* All naming helpers ({@link slugifyTicket}, {@link runBranchName}) are pure.
|
|
25
|
+
*/
|
|
26
|
+
/** Wall-clock cap on the best-effort worktree bootstrap install (spec §3b). */
|
|
27
|
+
export declare const BOOTSTRAP_TIMEOUT_MS: number;
|
|
28
|
+
/**
|
|
29
|
+
* Turn arbitrary ticket text into a git-ref-safe slug: lowercase, non-alnum runs
|
|
30
|
+
* collapse to `-`, trimmed, capped. Empty input (an all-symbol ticket) falls
|
|
31
|
+
* back to "run" so the branch name is always valid.
|
|
32
|
+
*/
|
|
33
|
+
export declare function slugifyTicket(ticket: string): string;
|
|
34
|
+
/** The run branch: `yagni/<ticket-slug>-<runShortId>` (spec §3b). */
|
|
35
|
+
export declare function runBranchName(ticket: string, runShortId: string): string;
|
|
36
|
+
/** Result of one shell-free exec: exit code + combined output. */
|
|
37
|
+
export interface ExecResult {
|
|
38
|
+
code: number;
|
|
39
|
+
output: string;
|
|
40
|
+
}
|
|
41
|
+
/** The impure exec seam (default: execFile with a scrubbed env); injected in tests. */
|
|
42
|
+
export type WorktreeExec = (argv: string[], cwd: string, signal?: AbortSignal) => Promise<ExecResult>;
|
|
43
|
+
export interface CreateWorktreeOpts {
|
|
44
|
+
/** The repo the user ran /go from (any path inside it works for git). */
|
|
45
|
+
repoCwd: string;
|
|
46
|
+
/** Absolute worktree destination (under ~/.yagni-code/worktrees/<shortId>). */
|
|
47
|
+
worktreePath: string;
|
|
48
|
+
/** The new run branch to create at HEAD. */
|
|
49
|
+
branch: string;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
/** Injectable git seam for tests. */
|
|
52
|
+
gitImpl?: (argv: string[], cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create the run worktree + branch FIRST: `git worktree add -b <branch> <path>
|
|
56
|
+
* HEAD`, with the parent worktrees dir created 0700. Throws (with git's stderr)
|
|
57
|
+
* on failure so the caller can degrade honestly to an in-place run.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createRunWorktree(opts: CreateWorktreeOpts): Promise<void>;
|
|
60
|
+
/** The bootstrap outcome: `note` is the honest fail-open explanation when it could not run. */
|
|
61
|
+
export interface BootstrapOutcome {
|
|
62
|
+
/** Did an install (or override) run to a clean exit? */
|
|
63
|
+
ran: boolean;
|
|
64
|
+
/** The command label (`pnpm install --prefer-offline` / the override line). */
|
|
65
|
+
command?: string;
|
|
66
|
+
/** Honest note when bootstrap was skipped or failed (surfaced to the user). */
|
|
67
|
+
note?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface BootstrapOpts {
|
|
70
|
+
readFile?: (path: string) => string;
|
|
71
|
+
exec?: WorktreeExec;
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Allow the repo-local `.yagni-code/bootstrap` override (same trust model as
|
|
75
|
+
* `.yagni-code/verify`: repo-defined commands only run when the operator
|
|
76
|
+
* opted in). Defaults to the same env gate verify uses.
|
|
77
|
+
*/
|
|
78
|
+
allowRepoOverride?: boolean;
|
|
79
|
+
signal?: AbortSignal;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Best-effort worktree bootstrap (layer 1 of spec §3b): install dependencies so
|
|
83
|
+
* verify/typecheck can run at all. Detection mirrors the verify gate's package
|
|
84
|
+
* manager detection; ONLY an install is run — never a repo build script we'd
|
|
85
|
+
* have to guess at. Every failure path is fail-open with an honest note; the
|
|
86
|
+
* verify gate's infra-failure classification (layer 2) covers whatever this
|
|
87
|
+
* could not provide.
|
|
88
|
+
*/
|
|
89
|
+
export declare function bootstrapWorktree(worktreePath: string, opts?: BootstrapOpts): Promise<BootstrapOutcome>;
|
|
90
|
+
/**
|
|
91
|
+
* Remove a run worktree via `git worktree remove` (run from the repo the user
|
|
92
|
+
* is in, so git also prunes its worktree metadata). NOT forced: git itself
|
|
93
|
+
* refuses a dirty worktree, which backstops /go-clean's own clean check.
|
|
94
|
+
* Throws with git's stderr on failure so the caller can report it honestly.
|
|
95
|
+
*/
|
|
96
|
+
export declare function removeWorktree(worktreePath: string, repoCwd: string, signal?: AbortSignal, gitImpl?: (argv: string[], cwd: string, signal?: AbortSignal) => Promise<string>): Promise<void>;
|
|
97
|
+
/** The WIP commit subject for a non-clean terminal outcome (house copy: no em-dashes). */
|
|
98
|
+
export declare function wipCommitMessage(ticket: string, stopReason: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Commit everything in the worktree as a clearly-marked WIP commit so a
|
|
101
|
+
* round_cap/partial/failed run's work is never lost and /go-clean can remove
|
|
102
|
+
* the (now clean) worktree. Returns the commit sha, or undefined when there was
|
|
103
|
+
* nothing to commit or the commit failed (fail-soft: an uncommittable worktree
|
|
104
|
+
* just stays dirty and /go-clean skips it with a note).
|
|
105
|
+
*/
|
|
106
|
+
export declare function commitWip(worktreePath: string, message: string, gitImpl?: (argv: string[], cwd: string, signal?: AbortSignal) => Promise<string>, signal?: AbortSignal): Promise<string | undefined>;
|
|
107
|
+
//# sourceMappingURL=worktree.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-worktree plumbing for worktree-by-default /go (spec §3b).
|
|
3
|
+
*
|
|
4
|
+
* A /go run gets its own detached worktree + run branch BEFORE anything else
|
|
5
|
+
* (`git worktree add -b yagni/<slug>-<shortId> ~/.yagni-code/worktrees/<shortId>
|
|
6
|
+
* HEAD`), so repoCtx, sessionKey, and every snapshot are resolved against the
|
|
7
|
+
* worktree path and the run can never fold the user's live tree into its diff.
|
|
8
|
+
*
|
|
9
|
+
* Three impure seams, each with the pure decision parts unit-testable:
|
|
10
|
+
* - {@link createRunWorktree} — the `git worktree add` shell-out (throws with
|
|
11
|
+
* git's stderr so the caller can fall back to in-place honestly).
|
|
12
|
+
* - {@link bootstrapWorktree} — best-effort `<pm> install --prefer-offline`
|
|
13
|
+
* with an 8-minute cap, because a fresh worktree has no node_modules and
|
|
14
|
+
* without it every verify/typecheck exits `command not found`. Fail-OPEN
|
|
15
|
+
* with an honest note; an opt-in `.yagni-code/bootstrap` file (same trust
|
|
16
|
+
* model as `.yagni-code/verify`: only read when the operator set
|
|
17
|
+
* `YAGNI_CODE_ALLOW_VERIFY_OVERRIDE=1`) overrides the detected install. The
|
|
18
|
+
* install runs under {@link buildVerifyEnv} so repo-triggered commands never
|
|
19
|
+
* inherit YAGNI/AI tokens (invariant §0.9).
|
|
20
|
+
* - {@link commitWip} — the WIP commit on a NON-clean terminal outcome
|
|
21
|
+
* (`wip(yagni): <ticket> - <stopReason>`), clearly marked and worktree-local,
|
|
22
|
+
* so round_cap/partial work is never lost and /go-clean can remove the tree.
|
|
23
|
+
*
|
|
24
|
+
* All naming helpers ({@link slugifyTicket}, {@link runBranchName}) are pure.
|
|
25
|
+
*/
|
|
26
|
+
import { execFile } from "node:child_process";
|
|
27
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { composeAbortSignal } from "./resilience.js";
|
|
30
|
+
import { buildVerifyEnv, detectPackageManager } from "./verify.js";
|
|
31
|
+
/** Wall-clock cap on the best-effort worktree bootstrap install (spec §3b). */
|
|
32
|
+
export const BOOTSTRAP_TIMEOUT_MS = 8 * 60_000;
|
|
33
|
+
/** Cap on the slug half of a run branch name (keeps refs readable). */
|
|
34
|
+
const SLUG_MAX = 40;
|
|
35
|
+
/**
|
|
36
|
+
* Turn arbitrary ticket text into a git-ref-safe slug: lowercase, non-alnum runs
|
|
37
|
+
* collapse to `-`, trimmed, capped. Empty input (an all-symbol ticket) falls
|
|
38
|
+
* back to "run" so the branch name is always valid.
|
|
39
|
+
*/
|
|
40
|
+
export function slugifyTicket(ticket) {
|
|
41
|
+
const slug = ticket
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
44
|
+
.replace(/^-+|-+$/g, "")
|
|
45
|
+
.slice(0, SLUG_MAX)
|
|
46
|
+
.replace(/-+$/, "");
|
|
47
|
+
return slug || "run";
|
|
48
|
+
}
|
|
49
|
+
/** The run branch: `yagni/<ticket-slug>-<runShortId>` (spec §3b). */
|
|
50
|
+
export function runBranchName(ticket, runShortId) {
|
|
51
|
+
return `yagni/${slugifyTicket(ticket)}-${runShortId}`;
|
|
52
|
+
}
|
|
53
|
+
const defaultExec = (argv, cwd, signal) => new Promise((resolve, reject) => {
|
|
54
|
+
execFile(argv[0], argv.slice(1), { cwd, signal, env: buildVerifyEnv(), maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
55
|
+
const output = `${stdout ?? ""}${stderr ?? ""}`;
|
|
56
|
+
if (err) {
|
|
57
|
+
const code = err.code;
|
|
58
|
+
if (typeof code === "number")
|
|
59
|
+
return resolve({ code, output });
|
|
60
|
+
return reject(err); // spawn error / abort — no numeric exit
|
|
61
|
+
}
|
|
62
|
+
resolve({ code: 0, output });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
/** Best-effort git in `cwd`; rejects with stderr detail on failure. */
|
|
66
|
+
function git(argv, cwd, signal) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
execFile("git", argv, { cwd, signal, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
69
|
+
if (err) {
|
|
70
|
+
reject(new Error(stderr.toString().trim() || err.message));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
resolve(stdout.toString().trim());
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create the run worktree + branch FIRST: `git worktree add -b <branch> <path>
|
|
79
|
+
* HEAD`, with the parent worktrees dir created 0700. Throws (with git's stderr)
|
|
80
|
+
* on failure so the caller can degrade honestly to an in-place run.
|
|
81
|
+
*/
|
|
82
|
+
export async function createRunWorktree(opts) {
|
|
83
|
+
const gitImpl = opts.gitImpl ?? git;
|
|
84
|
+
mkdirSync(dirname(opts.worktreePath), { recursive: true, mode: 0o700 });
|
|
85
|
+
await gitImpl(["worktree", "add", "-b", opts.branch, opts.worktreePath, "HEAD"], opts.repoCwd, opts.signal);
|
|
86
|
+
}
|
|
87
|
+
function tryRead(readFile, path) {
|
|
88
|
+
try {
|
|
89
|
+
return readFile(path);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** First non-comment, non-empty line of an override file, whitespace-split to argv. */
|
|
96
|
+
function parseOverride(body) {
|
|
97
|
+
for (const raw of body.split("\n")) {
|
|
98
|
+
const line = raw.trim();
|
|
99
|
+
if (!line || line.startsWith("#"))
|
|
100
|
+
continue;
|
|
101
|
+
const argv = line.split(/\s+/);
|
|
102
|
+
if (argv.length > 0)
|
|
103
|
+
return argv;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Best-effort worktree bootstrap (layer 1 of spec §3b): install dependencies so
|
|
109
|
+
* verify/typecheck can run at all. Detection mirrors the verify gate's package
|
|
110
|
+
* manager detection; ONLY an install is run — never a repo build script we'd
|
|
111
|
+
* have to guess at. Every failure path is fail-open with an honest note; the
|
|
112
|
+
* verify gate's infra-failure classification (layer 2) covers whatever this
|
|
113
|
+
* could not provide.
|
|
114
|
+
*/
|
|
115
|
+
export async function bootstrapWorktree(worktreePath, opts = {}) {
|
|
116
|
+
const readFile = opts.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
117
|
+
const exec = opts.exec ?? defaultExec;
|
|
118
|
+
const timeoutMs = opts.timeoutMs ?? BOOTSTRAP_TIMEOUT_MS;
|
|
119
|
+
const allowOverride = opts.allowRepoOverride ?? process.env.YAGNI_CODE_ALLOW_VERIFY_OVERRIDE === "1";
|
|
120
|
+
let argv = null;
|
|
121
|
+
let label = "";
|
|
122
|
+
const overrideBody = allowOverride ? tryRead(readFile, join(worktreePath, ".yagni-code", "bootstrap")) : null;
|
|
123
|
+
if (overrideBody) {
|
|
124
|
+
argv = parseOverride(overrideBody);
|
|
125
|
+
label = argv ? argv.join(" ") : "";
|
|
126
|
+
}
|
|
127
|
+
if (!argv) {
|
|
128
|
+
// No package.json at the worktree root => nothing we know how to bootstrap.
|
|
129
|
+
if (tryRead(readFile, join(worktreePath, "package.json")) == null) {
|
|
130
|
+
return { ran: false };
|
|
131
|
+
}
|
|
132
|
+
const pm = detectPackageManager(worktreePath, readFile);
|
|
133
|
+
argv = [pm, "install", "--prefer-offline"];
|
|
134
|
+
label = argv.join(" ");
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const signal = composeAbortSignal(opts.signal, AbortSignal.timeout(timeoutMs));
|
|
138
|
+
const { code, output } = await exec(argv, worktreePath, signal);
|
|
139
|
+
if (code !== 0) {
|
|
140
|
+
const tail = output.trim().split("\n").slice(-3).join(" ").slice(0, 200);
|
|
141
|
+
return {
|
|
142
|
+
ran: false,
|
|
143
|
+
command: label,
|
|
144
|
+
note: `worktree bootstrap (${label}) exited ${code}${tail ? `: ${tail}` : ""}. Verify/typecheck may not be able to run in this worktree.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { ran: true, command: label };
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
return {
|
|
152
|
+
ran: false,
|
|
153
|
+
command: label,
|
|
154
|
+
note: `worktree bootstrap (${label}) could not run: ${msg}. Verify/typecheck may not be able to run in this worktree.`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Remove a run worktree via `git worktree remove` (run from the repo the user
|
|
160
|
+
* is in, so git also prunes its worktree metadata). NOT forced: git itself
|
|
161
|
+
* refuses a dirty worktree, which backstops /go-clean's own clean check.
|
|
162
|
+
* Throws with git's stderr on failure so the caller can report it honestly.
|
|
163
|
+
*/
|
|
164
|
+
export async function removeWorktree(worktreePath, repoCwd, signal, gitImpl) {
|
|
165
|
+
const run = gitImpl ?? git;
|
|
166
|
+
await run(["worktree", "remove", worktreePath], repoCwd, signal);
|
|
167
|
+
}
|
|
168
|
+
/** The WIP commit subject for a non-clean terminal outcome (house copy: no em-dashes). */
|
|
169
|
+
export function wipCommitMessage(ticket, stopReason) {
|
|
170
|
+
return `wip(yagni): ${ticket} - ${stopReason}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Commit everything in the worktree as a clearly-marked WIP commit so a
|
|
174
|
+
* round_cap/partial/failed run's work is never lost and /go-clean can remove
|
|
175
|
+
* the (now clean) worktree. Returns the commit sha, or undefined when there was
|
|
176
|
+
* nothing to commit or the commit failed (fail-soft: an uncommittable worktree
|
|
177
|
+
* just stays dirty and /go-clean skips it with a note).
|
|
178
|
+
*/
|
|
179
|
+
export async function commitWip(worktreePath, message, gitImpl, signal) {
|
|
180
|
+
const run = gitImpl ?? git;
|
|
181
|
+
try {
|
|
182
|
+
const status = await run(["status", "--porcelain"], worktreePath, signal);
|
|
183
|
+
if (!status.trim())
|
|
184
|
+
return undefined; // already clean — nothing to preserve
|
|
185
|
+
await run(["add", "-A"], worktreePath, signal);
|
|
186
|
+
try {
|
|
187
|
+
await run(["commit", "-m", message], worktreePath, signal);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Most likely a missing git identity in a fresh environment; retry once
|
|
191
|
+
// with an explicit YAGNI Code identity rather than losing the WIP commit.
|
|
192
|
+
await run(["-c", "user.name=YAGNI Code", "-c", "user.email=code@yagni.app", "commit", "-m", message], worktreePath, signal);
|
|
193
|
+
}
|
|
194
|
+
return await run(["rev-parse", "HEAD"], worktreePath, signal);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined; // fail-soft: the worktree stays dirty; /go-clean reports it honestly
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=worktree.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ModelEntry } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the pi `ProviderConfig` for the `yagni` provider from a fetched catalog.
|
|
5
|
+
*
|
|
6
|
+
* The provider speaks the `openai-completions` API against `${baseUrl}/v1` and
|
|
7
|
+
* authenticates with the `$YAGNI_TOKEN` env var (resolved by pi at request time).
|
|
8
|
+
* pi — not this extension — performs the actual model calls.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildYagniProvider(catalog: ModelEntry[], baseUrl: string): ProviderConfig;
|
|
11
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the pi `ProviderConfig` for the `yagni` provider from a fetched catalog.
|
|
3
|
+
*
|
|
4
|
+
* The provider speaks the `openai-completions` API against `${baseUrl}/v1` and
|
|
5
|
+
* authenticates with the `$YAGNI_TOKEN` env var (resolved by pi at request time).
|
|
6
|
+
* pi — not this extension — performs the actual model calls.
|
|
7
|
+
*/
|
|
8
|
+
export function buildYagniProvider(catalog, baseUrl) {
|
|
9
|
+
const models = catalog.map((m) => ({
|
|
10
|
+
id: m.id,
|
|
11
|
+
name: m.name,
|
|
12
|
+
reasoning: m.reasoning ?? false,
|
|
13
|
+
input: m.input ?? ["text"],
|
|
14
|
+
cost: m.cost,
|
|
15
|
+
contextWindow: m.contextWindow,
|
|
16
|
+
maxTokens: m.maxTokens,
|
|
17
|
+
}));
|
|
18
|
+
return {
|
|
19
|
+
name: "YAGNI",
|
|
20
|
+
baseUrl: `${baseUrl}/v1`,
|
|
21
|
+
apiKey: "$YAGNI_TOKEN",
|
|
22
|
+
api: "openai-completions",
|
|
23
|
+
models,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient judgment recall (spec §4, M1 — degraded-tolerant).
|
|
3
|
+
*
|
|
4
|
+
* After the `read` tool returns, fetch the compact recorded judgment for that
|
|
5
|
+
* path (`GET /recall`) and append a `⟦YAGNI recall⟧ …` block to the tool result
|
|
6
|
+
* so the agent sees the decisions/corrections already banked for this file —
|
|
7
|
+
* zero extra turns, no model call.
|
|
8
|
+
*
|
|
9
|
+
* pi-recall-surface finding (verified against pi 0.80.2
|
|
10
|
+
* `dist/core/extensions/types.d.ts`): the interactive `tool_result` event is
|
|
11
|
+
* documented "Fired after a tool executes. Can modify result." and its handler
|
|
12
|
+
* result `ToolResultEventResult` exposes `content?: (TextContent | ImageContent)[]`.
|
|
13
|
+
* So RESULT MODIFICATION is supported: we return the original content plus the
|
|
14
|
+
* recall block. (The documented `before_agent_start` brief-injection fallback is
|
|
15
|
+
* therefore NOT needed here.)
|
|
16
|
+
*
|
|
17
|
+
* Everything is fail-soft and bounded:
|
|
18
|
+
* - `AbortSignal.timeout` + a hard deadline race cap the fetch at 1500ms, so
|
|
19
|
+
* recall NEVER blocks a read beyond the timeout — even if the shared
|
|
20
|
+
* authed-fetch seam attempts a 401 refresh underneath.
|
|
21
|
+
* - per-path session cache (a path is fetched at most once per session).
|
|
22
|
+
* - skipped entirely when the corpus is thin (<3 active decisions) or eval mode.
|
|
23
|
+
* - any failure degrades to no injection.
|
|
24
|
+
*/
|
|
25
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
/** Recall must never block a read beyond this (spec §4). */
|
|
27
|
+
export declare const RECALL_TIMEOUT_MS = 1500;
|
|
28
|
+
/** Below this many active decisions, recall is skipped entirely (spec: <3). */
|
|
29
|
+
export declare const RECALL_MIN_DECISIONS = 3;
|
|
30
|
+
/** Client-side caps mirror the backend (compact by construction). */
|
|
31
|
+
export declare const RECALL_MAX_DECISIONS = 5;
|
|
32
|
+
export declare const RECALL_MAX_CORRECTIONS = 3;
|
|
33
|
+
/** The compact recall payload from GET /api/yagni-code/recall (§2). */
|
|
34
|
+
export interface RecallDecision {
|
|
35
|
+
id: string;
|
|
36
|
+
question: string;
|
|
37
|
+
decision: string;
|
|
38
|
+
}
|
|
39
|
+
export interface RecallCorrection {
|
|
40
|
+
summary: string;
|
|
41
|
+
}
|
|
42
|
+
export interface RecallResult {
|
|
43
|
+
decisions: RecallDecision[];
|
|
44
|
+
corrections: RecallCorrection[];
|
|
45
|
+
/** True when the workspace has <3 active decisions total — skip injection. */
|
|
46
|
+
thin: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** Transport seam for {@link fetchRecall}. */
|
|
49
|
+
export interface RecallClientOpts {
|
|
50
|
+
baseUrl: string;
|
|
51
|
+
getToken: () => string | undefined;
|
|
52
|
+
fetchImpl?: typeof fetch;
|
|
53
|
+
/** Override the 1500ms cap (tests). */
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/** Optional repo full name to scope recall. */
|
|
56
|
+
repo?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Defensive parse of a /recall response into a {@link RecallResult}, or null. */
|
|
59
|
+
export declare function normalizeRecall(data: unknown): RecallResult | null;
|
|
60
|
+
/**
|
|
61
|
+
* Pure: derive the repo-relative path the backend expects from a read tool's
|
|
62
|
+
* `path` argument and the session cwd. Rebases an absolute path INSIDE the tree
|
|
63
|
+
* to a relative one (an outside path stays absolute), and strips a leading `./`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function toRepoRelativePath(readPath: string, cwd: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Pure: render the compact `⟦YAGNI recall⟧` block, or null when there is
|
|
68
|
+
* nothing to inject (missing result, thin corpus, or empty). No em-dashes.
|
|
69
|
+
*/
|
|
70
|
+
export declare function formatRecallBlock(result: RecallResult | null): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* GET /recall for a repo-relative path, bounded at {@link RECALL_TIMEOUT_MS}.
|
|
73
|
+
* Uses the shared authed fetch (inherits the 401 refresh-retry), but the hard
|
|
74
|
+
* deadline still caps the total wait. Returns null on any non-2xx, timeout, or
|
|
75
|
+
* error (fail-soft).
|
|
76
|
+
*/
|
|
77
|
+
export declare function fetchRecall(opts: RecallClientOpts, path: string, signal?: AbortSignal): Promise<RecallResult | null>;
|
|
78
|
+
/** Injectable deps for {@link registerAmbientRecall}. */
|
|
79
|
+
export interface RegisterRecallDeps extends RecallClientOpts {
|
|
80
|
+
/** Active-decision count from GET /context; recall is skipped when <3. */
|
|
81
|
+
decisionsCount?: number;
|
|
82
|
+
/** Eval mode disables recall entirely. */
|
|
83
|
+
evalMode?: boolean;
|
|
84
|
+
/** Recall fetcher (default: {@link fetchRecall}); injected in tests. */
|
|
85
|
+
fetchRecallImpl?: typeof fetchRecall;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Wire the `tool_result` handler that appends recall to a `read` result. No-op
|
|
89
|
+
* (nothing registered) when the corpus is thin (<3 decisions) or in eval mode,
|
|
90
|
+
* so there is zero overhead in those cases.
|
|
91
|
+
*/
|
|
92
|
+
export declare function registerAmbientRecall(pi: ExtensionAPI, deps: RegisterRecallDeps): void;
|
|
93
|
+
//# sourceMappingURL=recall.d.ts.map
|