Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gittensory-ams.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,12 @@ capLimits:
convergenceThresholds:
maxConsecutiveFailures: 3
maxReenqueues: 3

# Hard ceiling on the iterate loop's own iteration count for one attempt.
# A non-integer is floored; 0 abandons before the first driver invocation.
# Default: 3.
maxIterations: 3

# Per-iteration turn budget passed to the coding-agent driver.
# A non-integer is floored. Default: 6.
maxTurnsPerIteration: 6
4 changes: 2 additions & 2 deletions packages/gittensory-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,8 @@ the existence check — so a caller reads the returned path and feeds its conten
## AmsPolicySpec

`AmsPolicySpec` is the type surface for `.gittensory-ams.yml` — the OPERATOR's own execution-risk policy for
their miner (`submissionMode`, `slopThreshold`, `capLimits`, `convergenceThresholds`), a deliberate structural
sibling to `MinerGoalSpec` but answering a different question: `MinerGoalSpec` is what the target repo wants
their miner (`submissionMode`, `slopThreshold`, `capLimits`, `convergenceThresholds`, `maxIterations`,
`maxTurnsPerIteration`), a deliberate structural sibling to `MinerGoalSpec` but answering a different question: `MinerGoalSpec` is what the target repo wants
from being mined; `AmsPolicySpec` is how aggressive the operator wants their own agent to be. No field on this
type lets a target repo's own file loosen what an operator's agent is willing to do — see the type's own header
comment for why that boundary is load-bearing. `DEFAULT_AMS_POLICY_SPEC` is deny-by-default: `"observe"`
Expand Down
27 changes: 26 additions & 1 deletion packages/gittensory-engine/src/ams-policy-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export type AmsPolicySpec = {
capLimits: AmsCapLimits;
/** Non-convergence detector thresholds. Default: {@link DEFAULT_PORTFOLIO_CONVERGENCE_THRESHOLDS}. */
convergenceThresholds: PortfolioConvergenceThresholds;
/** Hard ceiling on the iterate loop's own iteration count (IterateLoopInput.maxIterations). Default: 3. */
maxIterations: number;
/** Per-iteration turn budget passed to the coding-agent driver (IterateLoopInput.maxTurnsPerIteration).
* Default: 6. */
maxTurnsPerIteration: number;
};

/** The tolerant parser result for `.gittensory-ams.yml`. Mirrors `ParsedMinerGoalSpec`'s present/warnings shape. */
Expand All @@ -70,6 +75,8 @@ export const DEFAULT_AMS_POLICY_SPEC: Readonly<AmsPolicySpec> = Object.freeze({
slopThreshold: "low",
capLimits: Object.freeze({ budget: 5, turns: 20, elapsedMs: 1_800_000 }),
convergenceThresholds: Object.freeze({ ...DEFAULT_PORTFOLIO_CONVERGENCE_THRESHOLDS }),
maxIterations: 3,
maxTurnsPerIteration: 6,
});

const MAX_AMS_POLICY_SPEC_BYTES = 8_192;
Expand All @@ -80,6 +87,8 @@ function cloneDefaultAmsPolicySpec(): AmsPolicySpec {
slopThreshold: DEFAULT_AMS_POLICY_SPEC.slopThreshold,
capLimits: { ...DEFAULT_AMS_POLICY_SPEC.capLimits },
convergenceThresholds: { ...DEFAULT_AMS_POLICY_SPEC.convergenceThresholds },
maxIterations: DEFAULT_AMS_POLICY_SPEC.maxIterations,
maxTurnsPerIteration: DEFAULT_AMS_POLICY_SPEC.maxTurnsPerIteration,
};
}

Expand Down Expand Up @@ -110,6 +119,13 @@ function normalizePositiveNumber(value: unknown, field: string, fallback: number
return value;
}

/** Like normalizePositiveNumber, but floors to a whole count -- for fields that are semantically integer
* counts (an iteration/turn budget), matching MinerGoalSpec's own normalizePositiveInteger convention. */
function normalizeNonNegativeInteger(value: unknown, field: string, fallback: number, warnings: string[]): number {
const normalized = normalizePositiveNumber(value, field, fallback, warnings);
return Math.floor(normalized);
}

function normalizeCapLimits(value: unknown, fallback: AmsCapLimits, warnings: string[]): AmsCapLimits {
if (value === undefined || value === null) return fallback;
if (typeof value !== "object" || Array.isArray(value)) {
Expand Down Expand Up @@ -154,7 +170,9 @@ function hasConfiguredPolicyFields(spec: AmsPolicySpec): boolean {
spec.capLimits.turns !== DEFAULT_AMS_POLICY_SPEC.capLimits.turns ||
spec.capLimits.elapsedMs !== DEFAULT_AMS_POLICY_SPEC.capLimits.elapsedMs ||
spec.convergenceThresholds.maxConsecutiveFailures !== DEFAULT_AMS_POLICY_SPEC.convergenceThresholds.maxConsecutiveFailures ||
spec.convergenceThresholds.maxReenqueues !== DEFAULT_AMS_POLICY_SPEC.convergenceThresholds.maxReenqueues
spec.convergenceThresholds.maxReenqueues !== DEFAULT_AMS_POLICY_SPEC.convergenceThresholds.maxReenqueues ||
spec.maxIterations !== DEFAULT_AMS_POLICY_SPEC.maxIterations ||
spec.maxTurnsPerIteration !== DEFAULT_AMS_POLICY_SPEC.maxTurnsPerIteration
);
}

Expand Down Expand Up @@ -190,6 +208,13 @@ export function parseAmsPolicySpec(raw: unknown): ParsedAmsPolicySpec {
DEFAULT_AMS_POLICY_SPEC.convergenceThresholds,
warnings,
),
maxIterations: normalizeNonNegativeInteger(record.maxIterations, "maxIterations", DEFAULT_AMS_POLICY_SPEC.maxIterations, warnings),
maxTurnsPerIteration: normalizeNonNegativeInteger(
record.maxTurnsPerIteration,
"maxTurnsPerIteration",
DEFAULT_AMS_POLICY_SPEC.maxTurnsPerIteration,
warnings,
),
};
if (!hasConfiguredPolicyFields(spec)) {
warnings.push("AmsPolicySpec contained no recognized non-default policy fields; falling back to safe defaults.");
Expand Down
22 changes: 22 additions & 0 deletions packages/gittensory-engine/test/ams-policy-spec-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ test("parseAmsPolicySpec: valid raw config normalizes every field and keeps non-
slopThreshold: "clean",
capLimits: { budget: 10, turns: 40, elapsedMs: 3_600_000 },
convergenceThresholds: { maxConsecutiveFailures: 5, maxReenqueues: 2 },
maxIterations: 5,
maxTurnsPerIteration: 10,
});

assert.equal(parsed.present, true);
Expand All @@ -46,10 +48,30 @@ test("parseAmsPolicySpec: valid raw config normalizes every field and keeps non-
slopThreshold: "clean",
capLimits: { budget: 10, turns: 40, elapsedMs: 3_600_000 },
convergenceThresholds: { maxConsecutiveFailures: 5, maxReenqueues: 2 },
maxIterations: 5,
maxTurnsPerIteration: 10,
});
assert.deepEqual(parsed.warnings, []);
});

test("parseAmsPolicySpec: maxIterations/maxTurnsPerIteration floor to whole counts and reject negative/non-numeric values", () => {
const floored = parseAmsPolicySpec({ maxIterations: 4.9, maxTurnsPerIteration: 8.2 });
assert.equal(floored.spec.maxIterations, 4);
assert.equal(floored.spec.maxTurnsPerIteration, 8);
assert.deepEqual(floored.warnings, []);

const zero = parseAmsPolicySpec({ maxIterations: 0, submissionMode: "enforce" });
assert.equal(zero.spec.maxIterations, 0);

const negative = parseAmsPolicySpec({ maxIterations: -1 });
assert.equal(negative.spec.maxIterations, DEFAULT_AMS_POLICY_SPEC.maxIterations);
assert.match(negative.warnings.join(" "), /maxIterations/i);

const nonNumeric = parseAmsPolicySpec({ maxTurnsPerIteration: "many" });
assert.equal(nonNumeric.spec.maxTurnsPerIteration, DEFAULT_AMS_POLICY_SPEC.maxTurnsPerIteration);
assert.match(nonNumeric.warnings.join(" "), /maxTurnsPerIteration/i);
});

test("parseAmsPolicySpec: submissionMode rejects an unrecognized value", () => {
const parsed = parseAmsPolicySpec({ submissionMode: "yolo" });
assert.equal(parsed.spec.submissionMode, "observe");
Expand Down
12 changes: 10 additions & 2 deletions packages/gittensory-miner/lib/attempt-cli.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { CodingAgentExecutionMode } from "@jsonbored/gittensory-engine";
import type { AttemptDeps } from "./attempt-runner.js";
import type { AttemptDeps, runMinerAttempt } from "./attempt-runner.js";
import type { ClaimLedger } from "./claim-ledger.js";
import type { EventLedger } from "./event-ledger.js";
import type { AttemptLog } from "./attempt-log.js";
import type { GovernorLedger } from "./governor-ledger.js";
import type { WorktreeAllocator } from "./worktree-allocator.js";
import type { resolveRejectionSignaled } from "./rejection-signal.js";
import type { SelfReviewContextFetch } from "./self-review-context.js";
import type { SelfReviewContextFetch, fetchSelfReviewContext } from "./self-review-context.js";
import type { cleanupAttemptWorktree, prepareAttemptWorktree } from "./attempt-worktree.js";
import type { buildCodingTaskSpec } from "./coding-task-spec.js";
import type { resolveAmsPolicy } from "./ams-policy.js";
import type { checkMinerKillSwitch } from "./governor-kill-switch.js";

export type ParsedAttemptArgs =
| { error: string }
Expand Down Expand Up @@ -35,6 +38,11 @@ export type RunAttemptOptions = {
fetchImpl?: SelfReviewContextFetch;
prepareAttemptWorktree?: typeof prepareAttemptWorktree;
cleanupAttemptWorktree?: typeof cleanupAttemptWorktree;
fetchSelfReviewContext?: typeof fetchSelfReviewContext;
buildCodingTaskSpec?: typeof buildCodingTaskSpec;
resolveAmsPolicy?: typeof resolveAmsPolicy;
checkMinerKillSwitch?: typeof checkMinerKillSwitch;
runMinerAttempt?: typeof runMinerAttempt;
};

export function runAttempt(args: string[], options?: RunAttemptOptions): Promise<number>;
Loading