@spikedpunch/align-agent 0.1.0
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 +21 -0
- package/README.md +83 -0
- package/dist/anthropicFixProvider.d.ts +33 -0
- package/dist/anthropicFixProvider.d.ts.map +1 -0
- package/dist/anthropicFixProvider.js +157 -0
- package/dist/anthropicFixProvider.js.map +1 -0
- package/dist/coverage.d.ts +15 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +28 -0
- package/dist/coverage.js.map +1 -0
- package/dist/effects.d.ts +69 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +2 -0
- package/dist/effects.js.map +1 -0
- package/dist/fixProvider.d.ts +51 -0
- package/dist/fixProvider.d.ts.map +1 -0
- package/dist/fixProvider.js +54 -0
- package/dist/fixProvider.js.map +1 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +47 -0
- package/dist/format.js.map +1 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +91 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/oscillation.d.ts +28 -0
- package/dist/oscillation.d.ts.map +1 -0
- package/dist/oscillation.js +31 -0
- package/dist/oscillation.js.map +1 -0
- package/dist/rails.d.ts +27 -0
- package/dist/rails.d.ts.map +1 -0
- package/dist/rails.js +38 -0
- package/dist/rails.js.map +1 -0
- package/dist/repairDecision.d.ts +23 -0
- package/dist/repairDecision.d.ts.map +1 -0
- package/dist/repairDecision.js +14 -0
- package/dist/repairDecision.js.map +1 -0
- package/dist/run.d.ts +68 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +211 -0
- package/dist/run.js.map +1 -0
- package/dist/symbolDiff.d.ts +16 -0
- package/dist/symbolDiff.d.ts.map +1 -0
- package/dist/symbolDiff.js +13 -0
- package/dist/symbolDiff.js.map +1 -0
- package/dist/symbolTable.d.ts +14 -0
- package/dist/symbolTable.d.ts.map +1 -0
- package/dist/symbolTable.js +10 -0
- package/dist/symbolTable.js.map +1 -0
- package/package.json +52 -0
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,UAAU,EAAsC,MAAM,cAAc,CAAC;AAiBpG,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CA0EhE"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real `git`/`gh` implementation of `GitEffects` — the only place in `@spikedpunch/align-agent` that shells
|
|
3
|
+
* out. No git library dependency exists anywhere else in this monorepo (confirmed at Stage-4
|
|
4
|
+
* research time), so this is net-new plumbing: thin wrappers over `git`/`gh` via
|
|
5
|
+
* `node:child_process`, never a shell string (execFile, not exec — no shell-injection surface
|
|
6
|
+
* from LLM-controlled commit messages/branch names).
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
async function git(rootDir, args) {
|
|
12
|
+
return execFileAsync('git', [...args], { cwd: rootDir, maxBuffer: 32 * 1024 * 1024 });
|
|
13
|
+
}
|
|
14
|
+
async function commandExists(cmd) {
|
|
15
|
+
try {
|
|
16
|
+
await execFileAsync(cmd, ['--version']);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function createNodeGitEffects(rootDir) {
|
|
24
|
+
return {
|
|
25
|
+
async isWorktreeClean() {
|
|
26
|
+
const { stdout } = await git(rootDir, ['status', '--porcelain']);
|
|
27
|
+
return stdout.trim().length === 0;
|
|
28
|
+
},
|
|
29
|
+
async currentBranch() {
|
|
30
|
+
const { stdout } = await git(rootDir, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
31
|
+
return stdout.trim();
|
|
32
|
+
},
|
|
33
|
+
async createBranch(name) {
|
|
34
|
+
await git(rootDir, ['checkout', '-b', name]);
|
|
35
|
+
},
|
|
36
|
+
async commit(message, paths) {
|
|
37
|
+
if (paths.length === 0)
|
|
38
|
+
throw new Error('commit() requires at least one path');
|
|
39
|
+
await git(rootDir, ['add', '--', ...paths]);
|
|
40
|
+
await git(rootDir, ['commit', '-m', message]);
|
|
41
|
+
const { stdout } = await git(rootDir, ['rev-parse', 'HEAD']);
|
|
42
|
+
return { sha: stdout.trim() };
|
|
43
|
+
},
|
|
44
|
+
async revertCommit(sha) {
|
|
45
|
+
// Work-branch commits are throwaway per REPAIR attempt — a hard reset to the commit's
|
|
46
|
+
// parent is the git-native "undo," not a revert-commit (no noise commits on a branch that
|
|
47
|
+
// may itself be discarded on escalation).
|
|
48
|
+
await git(rootDir, ['reset', '--hard', `${sha}~1`]);
|
|
49
|
+
},
|
|
50
|
+
async rebaseOnto(ontoBranch) {
|
|
51
|
+
try {
|
|
52
|
+
await git(rootDir, ['rebase', ontoBranch]);
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
await git(rootDir, ['rebase', '--abort']).catch(() => undefined);
|
|
57
|
+
return { ok: false, conflict: true };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async push(branch) {
|
|
61
|
+
const { stdout } = await git(rootDir, ['remote']);
|
|
62
|
+
if (stdout.trim().length === 0)
|
|
63
|
+
return { ok: false, reason: 'no-remote' };
|
|
64
|
+
try {
|
|
65
|
+
await git(rootDir, ['push', '-u', 'origin', branch]);
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { ok: false, reason: 'push-failed' };
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async createDraftPr(params) {
|
|
73
|
+
if (!(await commandExists('gh'))) {
|
|
74
|
+
return { ok: false, reason: 'gh CLI not found on PATH' };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'create', '--draft', '--base', params.base, '--head', params.branch, '--title', params.title, '--body', params.body], { cwd: rootDir });
|
|
78
|
+
return { ok: true, url: stdout.trim() };
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async ffMergeAndDeleteBranch(branch, base) {
|
|
85
|
+
await git(rootDir, ['checkout', base]);
|
|
86
|
+
await git(rootDir, ['merge', '--ff-only', branch]);
|
|
87
|
+
await git(rootDir, ['branch', '-d', branch]);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './fixProvider.js';
|
|
2
|
+
export * from './anthropicFixProvider.js';
|
|
3
|
+
export * from './coverage.js';
|
|
4
|
+
export * from './symbolTable.js';
|
|
5
|
+
export * from './symbolDiff.js';
|
|
6
|
+
export * from './oscillation.js';
|
|
7
|
+
export * from './rails.js';
|
|
8
|
+
export * from './repairDecision.js';
|
|
9
|
+
export * from './effects.js';
|
|
10
|
+
export * from './git.js';
|
|
11
|
+
export * from './format.js';
|
|
12
|
+
export * from './run.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './fixProvider.js';
|
|
2
|
+
export * from './anthropicFixProvider.js';
|
|
3
|
+
export * from './coverage.js';
|
|
4
|
+
export * from './symbolTable.js';
|
|
5
|
+
export * from './symbolDiff.js';
|
|
6
|
+
export * from './oscillation.js';
|
|
7
|
+
export * from './rails.js';
|
|
8
|
+
export * from './repairDecision.js';
|
|
9
|
+
export * from './effects.js';
|
|
10
|
+
export * from './git.js';
|
|
11
|
+
export * from './format.js';
|
|
12
|
+
export * from './run.js';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oscillation detection (REPAIR loop guard, ADR 012 shape-2 conflicts): per-file fingerprint-set
|
|
3
|
+
* history across REPAIR attempts. Fix A introduces violation B; fix B reintroduces violation A —
|
|
4
|
+
* the violation-id set returns to one already seen earlier in this group's history. That's a
|
|
5
|
+
* cycle in the state space, not progress — stop immediately and escalate naming both rule ids,
|
|
6
|
+
* rather than burning the remaining REPAIR budget ping-ponging.
|
|
7
|
+
*/
|
|
8
|
+
import type { RuleId, ViolationId } from '@spikedpunch/align-core';
|
|
9
|
+
export interface AttemptFingerprint {
|
|
10
|
+
readonly violationIds: ReadonlySet<ViolationId>;
|
|
11
|
+
readonly ruleIds: ReadonlySet<RuleId>;
|
|
12
|
+
}
|
|
13
|
+
export interface OscillationResult {
|
|
14
|
+
readonly oscillating: boolean;
|
|
15
|
+
/** Index into the history array of the earlier attempt whose fingerprint the latest attempt
|
|
16
|
+
* repeats — present only when `oscillating` is true. */
|
|
17
|
+
readonly repeatedAtIndex?: number;
|
|
18
|
+
/** Rule ids present in both the repeated attempt and the current one — named in the escalation
|
|
19
|
+
* report per the plan's "conflicting rules" requirement. */
|
|
20
|
+
readonly conflictingRuleIds?: readonly RuleId[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `history` is ordered oldest-to-newest, one entry per REPAIR attempt (including the initial
|
|
24
|
+
* DISCOVER state as index 0). Detects whether the LATEST entry's violation-id set exactly matches
|
|
25
|
+
* any earlier entry's set.
|
|
26
|
+
*/
|
|
27
|
+
export declare function detectOscillation(history: readonly AttemptFingerprint[]): OscillationResult;
|
|
28
|
+
//# sourceMappingURL=oscillation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oscillation.d.ts","sourceRoot":"","sources":["../src/oscillation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEnE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;4DACwD;IACxD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;gEAC4D;IAC5D,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjD;AAQD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,GAAG,iBAAiB,CAgB3F"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function setsEqual(a, b) {
|
|
2
|
+
if (a.size !== b.size)
|
|
3
|
+
return false;
|
|
4
|
+
for (const item of a)
|
|
5
|
+
if (!b.has(item))
|
|
6
|
+
return false;
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `history` is ordered oldest-to-newest, one entry per REPAIR attempt (including the initial
|
|
11
|
+
* DISCOVER state as index 0). Detects whether the LATEST entry's violation-id set exactly matches
|
|
12
|
+
* any earlier entry's set.
|
|
13
|
+
*/
|
|
14
|
+
export function detectOscillation(history) {
|
|
15
|
+
if (history.length < 2)
|
|
16
|
+
return { oscillating: false };
|
|
17
|
+
const latest = history[history.length - 1];
|
|
18
|
+
for (let i = 0; i < history.length - 1; i++) {
|
|
19
|
+
const earlier = history[i];
|
|
20
|
+
if (setsEqual(earlier.violationIds, latest.violationIds) && earlier.violationIds.size > 0) {
|
|
21
|
+
// Name every rule id seen across the whole repeated span (index i..latest), not just the
|
|
22
|
+
// two matching endpoints — "fix A introduces B, fix B reintroduces A" means the rule ids
|
|
23
|
+
// that changed hands mid-cycle (B) are exactly what the escalation report must name
|
|
24
|
+
// alongside the rule that oscillated back (A).
|
|
25
|
+
const conflictingRuleIds = [...new Set(history.slice(i).flatMap((h) => [...h.ruleIds]))];
|
|
26
|
+
return { oscillating: true, repeatedAtIndex: i, conflictingRuleIds };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { oscillating: false };
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=oscillation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oscillation.js","sourceRoot":"","sources":["../src/oscillation.ts"],"names":[],"mappings":"AAwBA,SAAS,SAAS,CAAI,CAAiB,EAAE,CAAiB;IACxD,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,CAAC;QAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;IACrD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsC;IACtE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAuB,CAAC;IAEjE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAuB,CAAC;QACjD,IAAI,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC1F,yFAAyF;YACzF,yFAAyF;YACzF,oFAAoF;YACpF,+CAA+C;YAC/C,MAAM,kBAAkB,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YACzF,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC;QACvE,CAAC;IACH,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC"}
|
package/dist/rails.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety-rail pure checks (IMPLEMENTATION_PLAN.md Stage 4 "Safety rails"). Kept separate from the
|
|
3
|
+
* git/fs imperative shell so every rail is independently unit-testable with plain data.
|
|
4
|
+
*/
|
|
5
|
+
import type { RepoRelativePath } from '@spikedpunch/align-core';
|
|
6
|
+
import type { FixProposal } from '@spikedpunch/align-core';
|
|
7
|
+
/** True if `rawPath` (as proposed by the LLM, not yet branded) targets `align.config.ts`,
|
|
8
|
+
* anything under `.align/`, or escapes the repo root (`..` traversal / absolute path). */
|
|
9
|
+
export declare function isForbiddenPath(rawPath: string): boolean;
|
|
10
|
+
export interface ForbiddenPathViolation {
|
|
11
|
+
readonly path: string;
|
|
12
|
+
}
|
|
13
|
+
/** Reject any `FixProposal` touching a forbidden path — checked BEFORE the apply pipeline ever
|
|
14
|
+
* runs, so a proposal that reaches for `align.config.ts` never gets a chance to validate an edit
|
|
15
|
+
* block against it. */
|
|
16
|
+
export declare function findForbiddenPathsInProposal(proposal: FixProposal): readonly ForbiddenPathViolation[];
|
|
17
|
+
/** Suppressions are dormant machinery in arch-first v1 (ADR 010/012): no lint gates exist yet, so
|
|
18
|
+
* no rule category is suppressible. Any proposal that uses `suppressions` is rejected outright —
|
|
19
|
+
* tested as such; see the agent package README for the one-paragraph explanation. */
|
|
20
|
+
export declare function usesSuppressions(proposal: FixProposal): boolean;
|
|
21
|
+
export type GroupFile = {
|
|
22
|
+
readonly file: RepoRelativePath;
|
|
23
|
+
};
|
|
24
|
+
/** Group violations by file — GROUP step (ADR 010/plan: "all of a file's violations in one
|
|
25
|
+
* prompt"). Pure, deterministic ordering (first-seen file order, stable). */
|
|
26
|
+
export declare function groupViolationsByFile<V extends GroupFile>(violations: readonly V[]): ReadonlyMap<RepoRelativePath, readonly V[]>;
|
|
27
|
+
//# sourceMappingURL=rails.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rails.d.ts","sourceRoot":"","sources":["../src/rails.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAK3D;0FAC0F;AAC1F,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAKxD;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;uBAEuB;AACvB,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,WAAW,GAAG,SAAS,sBAAsB,EAAE,CAErG;AAED;;qFAEqF;AACrF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAE/D;AAED,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE5D;6EAC6E;AAC7E,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,SAAS,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,GAAG,WAAW,CAAC,gBAAgB,EAAE,SAAS,CAAC,EAAE,CAAC,CAQhI"}
|
package/dist/rails.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const FORBIDDEN_PATH_PREFIXES = ['.align/', '.align'];
|
|
2
|
+
const FORBIDDEN_EXACT_FILES = ['align.config.ts'];
|
|
3
|
+
/** True if `rawPath` (as proposed by the LLM, not yet branded) targets `align.config.ts`,
|
|
4
|
+
* anything under `.align/`, or escapes the repo root (`..` traversal / absolute path). */
|
|
5
|
+
export function isForbiddenPath(rawPath) {
|
|
6
|
+
const normalized = rawPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
7
|
+
if (normalized.startsWith('/') || normalized.startsWith('..'))
|
|
8
|
+
return true;
|
|
9
|
+
if (FORBIDDEN_EXACT_FILES.includes(normalized))
|
|
10
|
+
return true;
|
|
11
|
+
return FORBIDDEN_PATH_PREFIXES.some((prefix) => normalized === prefix.replace(/\/$/, '') || normalized.startsWith(prefix));
|
|
12
|
+
}
|
|
13
|
+
/** Reject any `FixProposal` touching a forbidden path — checked BEFORE the apply pipeline ever
|
|
14
|
+
* runs, so a proposal that reaches for `align.config.ts` never gets a chance to validate an edit
|
|
15
|
+
* block against it. */
|
|
16
|
+
export function findForbiddenPathsInProposal(proposal) {
|
|
17
|
+
return proposal.files.filter((f) => isForbiddenPath(f.path)).map((f) => ({ path: f.path }));
|
|
18
|
+
}
|
|
19
|
+
/** Suppressions are dormant machinery in arch-first v1 (ADR 010/012): no lint gates exist yet, so
|
|
20
|
+
* no rule category is suppressible. Any proposal that uses `suppressions` is rejected outright —
|
|
21
|
+
* tested as such; see the agent package README for the one-paragraph explanation. */
|
|
22
|
+
export function usesSuppressions(proposal) {
|
|
23
|
+
return (proposal.suppressions?.length ?? 0) > 0;
|
|
24
|
+
}
|
|
25
|
+
/** Group violations by file — GROUP step (ADR 010/plan: "all of a file's violations in one
|
|
26
|
+
* prompt"). Pure, deterministic ordering (first-seen file order, stable). */
|
|
27
|
+
export function groupViolationsByFile(violations) {
|
|
28
|
+
const groups = new Map();
|
|
29
|
+
for (const v of violations) {
|
|
30
|
+
const list = groups.get(v.file);
|
|
31
|
+
if (list === undefined)
|
|
32
|
+
groups.set(v.file, [v]);
|
|
33
|
+
else
|
|
34
|
+
list.push(v);
|
|
35
|
+
}
|
|
36
|
+
return groups;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=rails.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rails.js","sourceRoot":"","sources":["../src/rails.ts"],"names":[],"mappings":"AAOA,MAAM,uBAAuB,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAU,CAAC;AAC/D,MAAM,qBAAqB,GAAG,CAAC,iBAAiB,CAAU,CAAC;AAE3D;0FAC0F;AAC1F,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACpE,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,IAAI,qBAAqB,CAAC,QAAQ,CAAC,UAAoD,CAAC;QAAE,OAAO,IAAI,CAAC;IACtG,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC7H,CAAC;AAMD;;uBAEuB;AACvB,MAAM,UAAU,4BAA4B,CAAC,QAAqB;IAChE,OAAO,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC9F,CAAC;AAED;;qFAEqF;AACrF,MAAM,UAAU,gBAAgB,CAAC,QAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AAClD,CAAC;AAID;6EAC6E;AAC7E,MAAM,UAAU,qBAAqB,CAAsB,UAAwB;IACjF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;;YAC3C,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure REPAIR decision core (CODING_BEST_PRACTICES.md §14: functional core, imperative shell).
|
|
3
|
+
* Given a group's attempt history, decides the next action — never touches git, the network, or
|
|
4
|
+
* the filesystem. Consumed by `run.ts`'s imperative shell.
|
|
5
|
+
*/
|
|
6
|
+
import type { RuleId } from '@spikedpunch/align-core';
|
|
7
|
+
import { type AttemptFingerprint } from './oscillation.js';
|
|
8
|
+
export type RepairDecision = {
|
|
9
|
+
readonly action: 'retry';
|
|
10
|
+
readonly attempt: number;
|
|
11
|
+
} | {
|
|
12
|
+
readonly action: 'escalate';
|
|
13
|
+
readonly reason: 'max-attempts';
|
|
14
|
+
} | {
|
|
15
|
+
readonly action: 'escalate-oscillation';
|
|
16
|
+
readonly reason: 'oscillation';
|
|
17
|
+
readonly conflictingRuleIds: readonly RuleId[];
|
|
18
|
+
};
|
|
19
|
+
/** `history` includes the initial (pre-fix) fingerprint at index 0 and one entry per completed
|
|
20
|
+
* attempt after that; `attemptsSoFar` is the count of REPAIR attempts already made (not counting
|
|
21
|
+
* the original PLAN+FIX). Max 3 REPAIR attempts per group, per the plan. */
|
|
22
|
+
export declare function decideNextRepairAction(history: readonly AttemptFingerprint[], attemptsSoFar: number, maxAttempts?: number): RepairDecision;
|
|
23
|
+
//# sourceMappingURL=repairDecision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repairDecision.d.ts","sourceRoot":"","sources":["../src/repairDecision.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE9E,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GAChE;IAAE,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEhI;;4EAE4E;AAC5E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,SAAS,kBAAkB,EAAE,EACtC,aAAa,EAAE,MAAM,EACrB,WAAW,SAAI,GACd,cAAc,CAOhB"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { detectOscillation } from './oscillation.js';
|
|
2
|
+
/** `history` includes the initial (pre-fix) fingerprint at index 0 and one entry per completed
|
|
3
|
+
* attempt after that; `attemptsSoFar` is the count of REPAIR attempts already made (not counting
|
|
4
|
+
* the original PLAN+FIX). Max 3 REPAIR attempts per group, per the plan. */
|
|
5
|
+
export function decideNextRepairAction(history, attemptsSoFar, maxAttempts = 3) {
|
|
6
|
+
const oscillation = detectOscillation(history);
|
|
7
|
+
if (oscillation.oscillating) {
|
|
8
|
+
return { action: 'escalate-oscillation', reason: 'oscillation', conflictingRuleIds: oscillation.conflictingRuleIds ?? [] };
|
|
9
|
+
}
|
|
10
|
+
if (attemptsSoFar >= maxAttempts)
|
|
11
|
+
return { action: 'escalate', reason: 'max-attempts' };
|
|
12
|
+
return { action: 'retry', attempt: attemptsSoFar + 1 };
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=repairDecision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repairDecision.js","sourceRoot":"","sources":["../src/repairDecision.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,iBAAiB,EAA2B,MAAM,kBAAkB,CAAC;AAO9E;;4EAE4E;AAC5E,MAAM,UAAU,sBAAsB,CACpC,OAAsC,EACtC,aAAqB,EACrB,WAAW,GAAG,CAAC;IAEf,MAAM,WAAW,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,WAAW,CAAC,WAAW,EAAE,CAAC;QAC5B,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,EAAE,aAAa,EAAE,kBAAkB,EAAE,WAAW,CAAC,kBAAkB,IAAI,EAAE,EAAE,CAAC;IAC7H,CAAC;IACD,IAAI,aAAa,IAAI,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACxF,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,GAAG,CAAC,EAAE,CAAC;AACzD,CAAC"}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The imperative shell (CODING_BEST_PRACTICES.md §14) that drives one `align agent run`:
|
|
3
|
+
* DISCOVER -> GROUP -> PLAN+FIX -> APPLY -> VERIFY -> REPAIR -> ESCALATE -> DONE -> TERMINAL MERGE.
|
|
4
|
+
* All I/O goes through `AgentEffects`; all decisions with real branching logic are pure functions
|
|
5
|
+
* imported from `repairDecision.ts`/`oscillation.ts`/`rails.ts`/`coverage.ts`/`symbolDiff.ts` —
|
|
6
|
+
* this file is deliberately thin sequencing, testable end-to-end with a fake `AgentEffects` +
|
|
7
|
+
* `FakeFixProvider` (see `test/run.test.ts`), never mocking a module.
|
|
8
|
+
*/
|
|
9
|
+
import { type CheckRun, type FixProposal, type RepoRelativePath, type RulesetIR } from '@spikedpunch/align-core';
|
|
10
|
+
import type { AgentEffects } from './effects.js';
|
|
11
|
+
export interface AgentRunOptions {
|
|
12
|
+
readonly maxAttempts: number;
|
|
13
|
+
readonly mode: 'pr' | 'auto-merge';
|
|
14
|
+
readonly allowUntested: boolean;
|
|
15
|
+
readonly allowSymbolRemovals: boolean;
|
|
16
|
+
readonly dryRun: boolean;
|
|
17
|
+
readonly workBranchName: string;
|
|
18
|
+
readonly baseBranch: string;
|
|
19
|
+
readonly prTitle?: string;
|
|
20
|
+
}
|
|
21
|
+
export type GroupOutcome = {
|
|
22
|
+
readonly status: 'done';
|
|
23
|
+
readonly file: RepoRelativePath;
|
|
24
|
+
readonly commitSha: string;
|
|
25
|
+
readonly rationale: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly status: 'escalated';
|
|
28
|
+
readonly file: RepoRelativePath;
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly status: 'dry-run';
|
|
32
|
+
readonly file: RepoRelativePath;
|
|
33
|
+
readonly proposal: FixProposal;
|
|
34
|
+
};
|
|
35
|
+
export type TerminalMergeOutcome = {
|
|
36
|
+
readonly status: 'no-commits';
|
|
37
|
+
} | {
|
|
38
|
+
readonly status: 'rebase-conflict';
|
|
39
|
+
} | {
|
|
40
|
+
readonly status: 'final-check-red';
|
|
41
|
+
readonly finalCheck: CheckRun;
|
|
42
|
+
} | {
|
|
43
|
+
readonly status: 'auto-merged';
|
|
44
|
+
} | {
|
|
45
|
+
readonly status: 'pr-created';
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly summary: string;
|
|
48
|
+
} | {
|
|
49
|
+
readonly status: 'no-remote-or-no-gh';
|
|
50
|
+
readonly summary: string;
|
|
51
|
+
readonly branch: string;
|
|
52
|
+
};
|
|
53
|
+
export type AgentRunVerdict = 'refused' | 'nothing-to-fix' | 'dry-run' | 'done' | 'partial-escalated';
|
|
54
|
+
export interface AgentRunResult {
|
|
55
|
+
readonly verdict: AgentRunVerdict;
|
|
56
|
+
readonly refusalReason?: string;
|
|
57
|
+
readonly groups: readonly GroupOutcome[];
|
|
58
|
+
readonly finalCheck?: CheckRun;
|
|
59
|
+
readonly terminalMerge?: TerminalMergeOutcome;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Top-level entry point. `ruleset` is passed as data (not an effect) — it's already loaded by the
|
|
63
|
+
* CLI composition root exactly as `align check` loads it, and is needed only to build
|
|
64
|
+
* `ruleExplanations` (pure).
|
|
65
|
+
*/
|
|
66
|
+
export declare function runAgentLoop(effects: AgentEffects, ruleset: RulesetIR, options: AgentRunOptions): Promise<AgentRunResult>;
|
|
67
|
+
export declare function defaultWorkBranchName(now?: () => number): string;
|
|
68
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,KAAK,QAAQ,EAEb,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAErB,KAAK,SAAS,EAEf,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AASjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC;IACnC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACpH;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1F;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC;AAEpG,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACjC;IAAE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,GACtC;IAAE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAA;CAAE,GACrE;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAClC;IAAE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjF;IAAE,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjG,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,mBAAmB,CAAC;AAEtG,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,oBAAoB,CAAC;CAC/C;AAqMD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAkC/H;AAED,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,MAAiB,GAAG,MAAM,CAG1E"}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The imperative shell (CODING_BEST_PRACTICES.md §14) that drives one `align agent run`:
|
|
3
|
+
* DISCOVER -> GROUP -> PLAN+FIX -> APPLY -> VERIFY -> REPAIR -> ESCALATE -> DONE -> TERMINAL MERGE.
|
|
4
|
+
* All I/O goes through `AgentEffects`; all decisions with real branching logic are pure functions
|
|
5
|
+
* imported from `repairDecision.ts`/`oscillation.ts`/`rails.ts`/`coverage.ts`/`symbolDiff.ts` —
|
|
6
|
+
* this file is deliberately thin sequencing, testable end-to-end with a fake `AgentEffects` +
|
|
7
|
+
* `FakeFixProvider` (see `test/run.test.ts`), never mocking a module.
|
|
8
|
+
*/
|
|
9
|
+
import { applyFixProposalFiles, toRepoRelativePath, toRuleId, } from '@spikedpunch/align-core';
|
|
10
|
+
import { buildCondensedSymbolTable } from './symbolTable.js';
|
|
11
|
+
import { isFileCovered } from './coverage.js';
|
|
12
|
+
import { diffExportedSymbols } from './symbolDiff.js';
|
|
13
|
+
import { decideNextRepairAction } from './repairDecision.js';
|
|
14
|
+
import { findForbiddenPathsInProposal, groupViolationsByFile, usesSuppressions } from './rails.js';
|
|
15
|
+
function ruleExplanationMap(ruleset) {
|
|
16
|
+
const map = new Map();
|
|
17
|
+
for (const rule of ruleset.rules) {
|
|
18
|
+
const because = rule.provenance.because ?? rule.provenance.sourceQuote;
|
|
19
|
+
const ruleId = toRuleId(rule.id);
|
|
20
|
+
map.set(ruleId, because === undefined ? { ruleId, kind: rule.kind } : { ruleId, kind: rule.kind, because });
|
|
21
|
+
}
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
function explanationsFor(violations, map) {
|
|
25
|
+
const ids = [...new Set(violations.map((v) => v.ruleId))];
|
|
26
|
+
return ids.map((id) => map.get(id) ?? { ruleId: id, kind: 'unknown' });
|
|
27
|
+
}
|
|
28
|
+
function fingerprintOf(violations) {
|
|
29
|
+
return { violationIds: new Set(violations.map((v) => v.id)), ruleIds: new Set(violations.map((v) => v.ruleId)) };
|
|
30
|
+
}
|
|
31
|
+
function escalationFromDecision(decision) {
|
|
32
|
+
if (decision.action === 'escalate-oscillation') {
|
|
33
|
+
return `oscillation detected — conflicting rules: ${decision.conflictingRuleIds.join(', ')} (fix A introduced B, fix B reintroduced A)`;
|
|
34
|
+
}
|
|
35
|
+
return 'exceeded the maximum REPAIR attempts for this group';
|
|
36
|
+
}
|
|
37
|
+
async function buildInputForFile(effects, file, violations, explanations, previousFailure) {
|
|
38
|
+
const graph = await effects.scanGraph();
|
|
39
|
+
const fileContent = await effects.readFile(file);
|
|
40
|
+
return {
|
|
41
|
+
violations,
|
|
42
|
+
fileContents: new Map([[file, fileContent]]),
|
|
43
|
+
condensedSymbolTable: buildCondensedSymbolTable(file, graph),
|
|
44
|
+
ruleExplanations: explanationsFor(violations, explanations),
|
|
45
|
+
...(previousFailure !== undefined ? { previousFailure } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** DISCOVER + GROUP + PLAN only — used both for `--dry-run` and internally is NOT reused for the
|
|
49
|
+
* real run (the real run needs the full per-group loop below, not just one proposal). */
|
|
50
|
+
async function planOnly(effects, groupsMap, explanations) {
|
|
51
|
+
const outcomes = [];
|
|
52
|
+
for (const [file, violations] of groupsMap) {
|
|
53
|
+
const input = await buildInputForFile(effects, file, violations, explanations);
|
|
54
|
+
const proposal = await effects.fixProvider.proposeFix(input);
|
|
55
|
+
outcomes.push({ status: 'dry-run', file, proposal });
|
|
56
|
+
}
|
|
57
|
+
return outcomes;
|
|
58
|
+
}
|
|
59
|
+
/** PLAN+FIX -> APPLY -> VERIFY -> REPAIR for one file GROUP. Returns once the group reaches DONE
|
|
60
|
+
* or ESCALATE. */
|
|
61
|
+
async function runGroup(effects, file, initialViolations, explanations, options) {
|
|
62
|
+
// Green≠correct guard (b): zero-coverage refusal — checked once, before any PLAN+FIX call.
|
|
63
|
+
if (!options.allowUntested) {
|
|
64
|
+
const graph = await effects.scanGraph();
|
|
65
|
+
if (!isFileCovered(file, graph)) {
|
|
66
|
+
return {
|
|
67
|
+
status: 'escalated',
|
|
68
|
+
file,
|
|
69
|
+
reason: 'zero test coverage — no scanned test file transitively imports this file (pass --allow-untested to override)',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const history = [fingerprintOf(initialViolations)];
|
|
74
|
+
let attemptsSoFar = 0;
|
|
75
|
+
let currentViolations = initialViolations;
|
|
76
|
+
let previousFailure;
|
|
77
|
+
for (;;) {
|
|
78
|
+
const input = await buildInputForFile(effects, file, currentViolations, explanations, previousFailure);
|
|
79
|
+
const proposal = await effects.fixProvider.proposeFix(input);
|
|
80
|
+
const forbidden = findForbiddenPathsInProposal(proposal);
|
|
81
|
+
if (forbidden.length > 0) {
|
|
82
|
+
return { status: 'escalated', file, reason: `proposal touched a forbidden path: ${forbidden.map((f) => f.path).join(', ')}` };
|
|
83
|
+
}
|
|
84
|
+
if (usesSuppressions(proposal)) {
|
|
85
|
+
return { status: 'escalated', file, reason: 'no suppressible rule categories active — suppressions field is dormant in v1' };
|
|
86
|
+
}
|
|
87
|
+
const originals = new Map();
|
|
88
|
+
for (const f of proposal.files) {
|
|
89
|
+
const p = toRepoRelativePath(f.path);
|
|
90
|
+
originals.set(p, await effects.readFile(p));
|
|
91
|
+
}
|
|
92
|
+
const validated = applyFixProposalFiles(originals, proposal.files, toRepoRelativePath);
|
|
93
|
+
const applyFailure = validated.find((v) => !v.ok);
|
|
94
|
+
if (applyFailure !== undefined && !applyFailure.ok) {
|
|
95
|
+
attemptsSoFar += 1;
|
|
96
|
+
const decision = decideNextRepairAction(history, attemptsSoFar, options.maxAttempts);
|
|
97
|
+
if (decision.action !== 'retry')
|
|
98
|
+
return { status: 'escalated', file, reason: escalationFromDecision(decision) };
|
|
99
|
+
previousFailure = applyFailure.failure;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const touchedPaths = validated.filter((v) => v.ok).map((v) => v.path);
|
|
103
|
+
const graphBefore = await effects.scanGraph();
|
|
104
|
+
for (const v of validated)
|
|
105
|
+
if (v.ok)
|
|
106
|
+
await effects.writeFile(v.path, v.content);
|
|
107
|
+
await effects.formatIfAvailable(touchedPaths);
|
|
108
|
+
// Green≠correct guard (a): exported-symbol surface diff.
|
|
109
|
+
const graphAfter = await effects.scanGraph();
|
|
110
|
+
const before = touchedPaths.map((p) => ({ file: p, exports: graphBefore.nodes.find((n) => n.file === p)?.exports ?? [] }));
|
|
111
|
+
const after = touchedPaths.map((p) => ({ file: p, exports: graphAfter.nodes.find((n) => n.file === p)?.exports ?? [] }));
|
|
112
|
+
const removals = diffExportedSymbols(before, after);
|
|
113
|
+
if (removals.length > 0 && !options.allowSymbolRemovals) {
|
|
114
|
+
for (const [p, content] of originals)
|
|
115
|
+
await effects.writeFile(p, content); // revert uncommitted writes
|
|
116
|
+
return {
|
|
117
|
+
status: 'escalated',
|
|
118
|
+
file,
|
|
119
|
+
reason: `exported-symbol removal requires --allow-symbol-removals: ${removals.map((r) => `${r.file}(${r.removedSymbols.join(',')})`).join('; ')}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const { sha } = await effects.git.commit(proposal.rationale, touchedPaths);
|
|
123
|
+
const checkRun = await effects.runCheck();
|
|
124
|
+
if (checkRun.verdict === 'error') {
|
|
125
|
+
return { status: 'escalated', file, reason: 'gate error during VERIFY — environmental, halting this group' };
|
|
126
|
+
}
|
|
127
|
+
const remaining = checkRun.gates.flatMap((g) => g.violations).filter((v) => touchedPaths.includes(v.file));
|
|
128
|
+
if (remaining.length === 0) {
|
|
129
|
+
return { status: 'done', file, commitSha: sha, rationale: proposal.rationale };
|
|
130
|
+
}
|
|
131
|
+
history.push(fingerprintOf(remaining));
|
|
132
|
+
attemptsSoFar += 1;
|
|
133
|
+
const decision = decideNextRepairAction(history, attemptsSoFar, options.maxAttempts);
|
|
134
|
+
await effects.git.revertCommit(sha);
|
|
135
|
+
if (decision.action !== 'retry')
|
|
136
|
+
return { status: 'escalated', file, reason: escalationFromDecision(decision) };
|
|
137
|
+
currentViolations = remaining;
|
|
138
|
+
previousFailure = undefined; // a "still red" retry is not an apply-mismatch retry
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function renderPrSummary(doneGroups) {
|
|
142
|
+
const lines = ['## Violations fixed by `align agent run`', ''];
|
|
143
|
+
for (const g of doneGroups)
|
|
144
|
+
lines.push(`- \`${g.file}\` (${g.commitSha.slice(0, 7)}): ${g.rationale}`);
|
|
145
|
+
return `${lines.join('\n')}\n`;
|
|
146
|
+
}
|
|
147
|
+
async function performTerminalMerge(effects, options, groups) {
|
|
148
|
+
const doneGroups = groups.filter((g) => g.status === 'done');
|
|
149
|
+
if (doneGroups.length === 0)
|
|
150
|
+
return { status: 'no-commits' };
|
|
151
|
+
const rebase = await effects.git.rebaseOnto(options.baseBranch);
|
|
152
|
+
if (!rebase.ok)
|
|
153
|
+
return { status: 'rebase-conflict' };
|
|
154
|
+
const finalCheck = await effects.runCheck();
|
|
155
|
+
if (finalCheck.verdict !== 'green')
|
|
156
|
+
return { status: 'final-check-red', finalCheck };
|
|
157
|
+
if (options.mode === 'auto-merge') {
|
|
158
|
+
await effects.git.ffMergeAndDeleteBranch(options.workBranchName, options.baseBranch);
|
|
159
|
+
return { status: 'auto-merged' };
|
|
160
|
+
}
|
|
161
|
+
const summary = renderPrSummary(doneGroups);
|
|
162
|
+
const pushResult = await effects.git.push(options.workBranchName);
|
|
163
|
+
if (!pushResult.ok)
|
|
164
|
+
return { status: 'no-remote-or-no-gh', summary, branch: options.workBranchName };
|
|
165
|
+
const pr = await effects.git.createDraftPr({
|
|
166
|
+
branch: options.workBranchName,
|
|
167
|
+
base: options.baseBranch,
|
|
168
|
+
title: options.prTitle ?? `align: automated fixes (${options.workBranchName})`,
|
|
169
|
+
body: summary,
|
|
170
|
+
});
|
|
171
|
+
if (!pr.ok)
|
|
172
|
+
return { status: 'no-remote-or-no-gh', summary, branch: options.workBranchName };
|
|
173
|
+
return { status: 'pr-created', url: pr.url, summary };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Top-level entry point. `ruleset` is passed as data (not an effect) — it's already loaded by the
|
|
177
|
+
* CLI composition root exactly as `align check` loads it, and is needed only to build
|
|
178
|
+
* `ruleExplanations` (pure).
|
|
179
|
+
*/
|
|
180
|
+
export async function runAgentLoop(effects, ruleset, options) {
|
|
181
|
+
if (!(await effects.git.isWorktreeClean())) {
|
|
182
|
+
return { verdict: 'refused', refusalReason: 'dirty worktree — commit or stash changes before running the agent', groups: [] };
|
|
183
|
+
}
|
|
184
|
+
const initialCheck = await effects.runCheck();
|
|
185
|
+
if (initialCheck.verdict === 'error') {
|
|
186
|
+
return { verdict: 'refused', refusalReason: 'gate error on initial check — environmental, not fixable by the agent', groups: [], finalCheck: initialCheck };
|
|
187
|
+
}
|
|
188
|
+
const violations = initialCheck.gates.flatMap((g) => g.violations);
|
|
189
|
+
if (violations.length === 0) {
|
|
190
|
+
return { verdict: 'nothing-to-fix', groups: [], finalCheck: initialCheck };
|
|
191
|
+
}
|
|
192
|
+
const groupsMap = groupViolationsByFile(violations);
|
|
193
|
+
const explanations = ruleExplanationMap(ruleset);
|
|
194
|
+
if (options.dryRun) {
|
|
195
|
+
const groups = await planOnly(effects, groupsMap, explanations);
|
|
196
|
+
return { verdict: 'dry-run', groups };
|
|
197
|
+
}
|
|
198
|
+
await effects.git.createBranch(options.workBranchName);
|
|
199
|
+
const groups = [];
|
|
200
|
+
for (const [file, groupViolations] of groupsMap) {
|
|
201
|
+
groups.push(await runGroup(effects, file, groupViolations, explanations, options));
|
|
202
|
+
}
|
|
203
|
+
const anyEscalated = groups.some((g) => g.status === 'escalated');
|
|
204
|
+
const terminalMerge = await performTerminalMerge(effects, options, groups);
|
|
205
|
+
return { verdict: anyEscalated ? 'partial-escalated' : 'done', groups, terminalMerge };
|
|
206
|
+
}
|
|
207
|
+
export function defaultWorkBranchName(now = Date.now) {
|
|
208
|
+
const iso = new Date(now()).toISOString().slice(0, 10);
|
|
209
|
+
return `align/fixes-${iso}`;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=run.js.map
|