looply-cli 0.5.18 → 0.5.19

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/dist/run-cmd.js CHANGED
@@ -73,6 +73,7 @@ const run_base_2 = require("./run-base");
73
73
  const round_records_1 = require("./round-records");
74
74
  const goal_gap_1 = require("./goal-gap");
75
75
  const visual_proof_run_1 = require("./visual-proof-run");
76
+ const bug_repro_1 = require("./bug-repro");
76
77
  const goal_adaptive_1 = require("./goal-adaptive");
77
78
  const pending_action_1 = require("./pending-action");
78
79
  const approval_resolution_1 = require("./approval-resolution");
@@ -2875,6 +2876,9 @@ async function runLoop(opts) {
2875
2876
  const compileReaders = (0, citation_readers_1.nodeCitationReaders)(cwd, stateDir);
2876
2877
  const compileProvider = (0, provider_identity_1.providerRecordFor)(resolved.spec, resolved.spec.command === agent_exec_1.DEFAULT_AGENT.command ? (0, provider_discovery_1.claudeExecutable)(resolved.spec.executable) : null);
2877
2878
  const compiles = new compile_store_1.CompileStore(stateDir, ledger_store_1.nodeLedgerFs);
2879
+ // Per PROJECT, like the goal mirror: the reproduction a Start recorded is the one every later Start reads.
2880
+ const reproStore = new bug_repro_1.ReproStore(projectStateDirPath);
2881
+ const reproFor = (id, version) => reproStore.read(id, version);
2878
2882
  const recordCompile = async (forGoalId, forGoalVersion, treeSha, outcome, checked) => {
2879
2883
  try {
2880
2884
  if (!compiles.isOpen())
@@ -2930,17 +2934,62 @@ async function runLoop(opts) {
2930
2934
  /** The first compile's authority answer, kept so its reason is the acted-on one. */
2931
2935
  let stillOurs = { ok: true };
2932
2936
  let seamFailure = null;
2937
+ /** A bug that did not show: the one question for the ticket, asked before any plan (`bug-repro.ts`). */
2938
+ let reproAsks = null;
2933
2939
  try {
2934
- if (verdict.compile && scope !== null && binding.kind === 'bound') {
2940
+ /**
2941
+ * BUG TICKETS ARE REPRODUCTION-FIRST (`bug-repro.ts`). Before a bug goal's first plan, a test
2942
+ * is written and run red on the base tree, and its text travels into the plan and every round.
2943
+ * A bug that does not show asks one question instead of spending a fix attempt — only when the
2944
+ * reproduction happened in THIS Start, so a resumed goal is never asked the same thing twice.
2945
+ */
2946
+ if (verdict.compile && scope !== null && binding.kind === 'bound' && (0, bug_repro_1.isBugGoal)(binding.goal.text)) {
2947
+ let repro = reproStore.read(binding.goal.id, binding.goal.version);
2948
+ if (repro === null) {
2949
+ log('repro : this is a bug — writing a test that shows it before planning the fix');
2950
+ repro = await (0, bug_repro_1.reproduceBug)({
2951
+ goalId: binding.goal.id,
2952
+ goalVersion: binding.goal.version,
2953
+ goalText: binding.goal.text,
2954
+ projectName: config.project.name,
2955
+ gates: config.gates,
2956
+ }, {
2957
+ invoke: agent,
2958
+ run: (0, untrusted_gate_1.makeUntrustedGateRunner)(cwd),
2959
+ git: { changedEntries: () => git.changedEntries(), headSha: () => git.headSha(), discard: () => git.discard() },
2960
+ readFile: (0, bug_repro_1.checkoutReader)(cwd),
2961
+ download: (0, bug_repro_1.nodeAttachmentDownloader)(cwd),
2962
+ now: () => new Date(),
2963
+ log,
2964
+ });
2965
+ try {
2966
+ reproStore.write(repro);
2967
+ }
2968
+ catch (e) {
2969
+ log(`repro : the record could not be written — ${e.message}`);
2970
+ }
2971
+ log(repro.outcome === 'reproduced'
2972
+ ? `repro : reproduced — "${repro.test}" fails on the base tree (exit ${repro.failedBefore?.exitCode ?? '?'})`
2973
+ : `repro : ${repro.outcome} — ${repro.because ?? ''}`);
2974
+ if (repro.outcome === 'question')
2975
+ reproAsks = (0, bug_repro_1.reproQuestion)(repro);
2976
+ }
2977
+ else if (repro.outcome === 'question') {
2978
+ log('repro : a question was already asked for this goal; going on without a reproduction');
2979
+ }
2980
+ }
2981
+ if (reproAsks === null && verdict.compile && scope !== null && binding.kind === 'bound') {
2935
2982
  compiledThisRun = true;
2936
2983
  log(`compile : ${binding.goal.text}`);
2937
2984
  // Read before the compile: the compiler is told not to change anything, and every check of its
2938
2985
  // answer (the first and any re-plan) is made against the same committed tree.
2939
2986
  const compiledAt = await git.headTreeSha().catch(() => null);
2987
+ // A reproduced bug's test rides with the goal's words: the plan's first job is to make it pass.
2988
+ const planText = (0, bug_repro_1.withReproduction)(binding.goal.text, reproFor(binding.goal.id, binding.goal.version));
2940
2989
  const recovered = await (0, plan_recovery_1.compileWithRecovery)({
2941
2990
  goalId: binding.goal.id,
2942
2991
  goalVersion: binding.goal.version,
2943
- goalText: binding.goal.text,
2992
+ goalText: planText,
2944
2993
  projectId,
2945
2994
  projectName: config.project.name,
2946
2995
  /**
@@ -3291,7 +3340,7 @@ async function runLoop(opts) {
3291
3340
  adaptive = await (0, goal_adaptive_1.decideAdaptive)({
3292
3341
  goalId: scope.goalId,
3293
3342
  goalVersion: bound.version,
3294
- goalText: bound.text,
3343
+ goalText: (0, bug_repro_1.withReproduction)(bound.text, reproFor(scope.goalId, bound.version)),
3295
3344
  projectId,
3296
3345
  projectName: config.project.name,
3297
3346
  currentRevision: revision,
@@ -3337,7 +3386,18 @@ async function runLoop(opts) {
3337
3386
  ...(config.run.baseBranch === undefined ? {} : { configuredBase: config.run.baseBranch }),
3338
3387
  log,
3339
3388
  });
3340
- return (0, goal_gap_1.evaluateGoalGap)(screenshots.length > 0 ? { ...i, screenshots } : i, { invoke: planAgent });
3389
+ // A reproduced bug is met only when its test passes at this tree, run by the machine (`bug-repro.ts`).
3390
+ const gap = await (0, goal_gap_1.evaluateGoalGap)(screenshots.length > 0 ? { ...i, screenshots } : i, { invoke: planAgent });
3391
+ return (0, bug_repro_1.evaluateWithReproduction)(gap, reproFor(goalId, bound.version), {
3392
+ run: (0, untrusted_gate_1.makeUntrustedGateRunner)(cwd),
3393
+ readFile: (0, bug_repro_1.checkoutReader)(cwd),
3394
+ headSha: () => git.headSha(),
3395
+ changedPaths: () => git.changedPaths(),
3396
+ discard: () => git.discard(),
3397
+ store: reproStore,
3398
+ now: () => new Date(),
3399
+ log,
3400
+ });
3341
3401
  },
3342
3402
  // The same recovery as the first plan: re-plan refused steps, read an empty answer as
3343
3403
  // an instruction, and fall back to one direct step before this revision is empty.
@@ -3598,6 +3658,8 @@ async function runLoop(opts) {
3598
3658
  * of that block and the ending is written there, before any of them.
3599
3659
  */
3600
3660
  const nothingPickableBecause = () => {
3661
+ if (reproAsks !== null)
3662
+ return reproAsks;
3601
3663
  if (seamFailure !== null)
3602
3664
  return `could not check this goal — ${seamFailure}`;
3603
3665
  const said = adaptive === null ? null : (0, goal_adaptive_1.describeAdaptive)(adaptive, attestedNow(), coverage);
@@ -3700,6 +3762,11 @@ async function runLoop(opts) {
3700
3762
  // disk. That is safe — the write is one whole-file rename and the next
3701
3763
  // Start converges on the same rows — but the message should not claim more
3702
3764
  // than it knows, so it says what happened rather than what it hopes.
3765
+ // The bug did not show: one precise question on the ticket, before any fix is attempted.
3766
+ if (reproAsks !== null) {
3767
+ await releaseHold();
3768
+ throw new RunRefused(reproAsks, 'human_required');
3769
+ }
3703
3770
  if (seamFailure !== null) {
3704
3771
  await releaseHold();
3705
3772
  throw new Error(`could not check this goal — ${seamFailure}`);
@@ -3951,7 +4018,7 @@ async function runLoop(opts) {
3951
4018
  * bound to — a second read could straddle an amendment and show a
3952
4019
  * goal this work was never authorised against.
3953
4020
  */
3954
- binding.kind === 'bound' ? binding.goal.text : undefined,
4021
+ binding.kind === 'bound' ? (0, bug_repro_1.withReproduction)(binding.goal.text, reproFor(binding.goal.id, binding.goal.version)) : undefined,
3955
4022
  // The other repositories this change spans, resolved once for the run.
3956
4023
  span),
3957
4024
  timeoutMs: AGENT_TIMEOUT_MS,