opencode-swarm 7.86.0 → 7.87.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/dist/cli/{capability-probe-jevmgwmf.js → capability-probe-wsjzcp48.js} +2 -2
- package/dist/cli/{config-doctor-zejarrr6.js → config-doctor-6h64pn8n.js} +4 -4
- package/dist/cli/{dispatch-k86d928w.js → dispatch-kb69qw40.js} +3 -3
- package/dist/cli/{evidence-summary-service-g2znnd33.js → evidence-summary-service-gg5m9z57.js} +4 -4
- package/dist/cli/{guardrail-explain-rtd1x26f.js → guardrail-explain-scym5r5y.js} +13 -13
- package/dist/cli/{guardrail-log-80116wmz.js → guardrail-log-eegabqcp.js} +5 -5
- package/dist/cli/{index-0sxvwjt0.js → index-1cb4wxnm.js} +2 -2
- package/dist/cli/{index-zfsbaaqh.js → index-5e4e2hvv.js} +1 -1
- package/dist/cli/{index-vq2321gg.js → index-5hvbw5xh.js} +2 -2
- package/dist/cli/{index-5cb86007.js → index-5vpe6vq9.js} +1 -1
- package/dist/cli/{index-red8fm8p.js → index-89xjr3h4.js} +1162 -214
- package/dist/cli/{index-f8r50m3h.js → index-adz3nk9b.js} +2 -2
- package/dist/cli/{index-jwz50183.js → index-dsjyfd3g.js} +14 -14
- package/dist/cli/{index-ckntc5gf.js → index-gn8n22th.js} +2 -2
- package/dist/cli/{index-5q66xc88.js → index-gwzpy671.js} +2699 -1403
- package/dist/cli/{index-hw9b2xng.js → index-q9h0wb04.js} +36 -3
- package/dist/cli/{index-d9fbxaqd.js → index-s8bj492g.js} +1 -1
- package/dist/cli/{index-7r2b453y.js → index-ts2j1wjr.js} +2 -2
- package/dist/cli/{index-hz59hg4h.js → index-v4fcn4tr.js} +1 -1
- package/dist/cli/{index-eb85wtx9.js → index-vqyfscxd.js} +2 -2
- package/dist/cli/{index-yx44zd0p.js → index-zgwm4ryv.js} +9 -1
- package/dist/cli/index.js +12 -12
- package/dist/cli/{pending-delegations-rd40tv9s.js → pending-delegations-35fvcj7z.js} +3 -3
- package/dist/cli/{pr-subscriptions-y1nn36e5.js → pr-subscriptions-b18n1yd8.js} +4 -4
- package/dist/cli/{schema-8d32b2v6.js → schema-84146tvk.js} +3 -1
- package/dist/cli/{skill-generator-a5ehggyg.js → skill-generator-3pvpk4y2.js} +2 -2
- package/dist/commands/coupling.d.ts +36 -0
- package/dist/commands/epic.d.ts +52 -0
- package/dist/commands/registry.d.ts +18 -2
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/schema.d.ts +145 -0
- package/dist/git/branch.d.ts +22 -1
- package/dist/hooks/delegation-gate/worktree-merge-status.d.ts +86 -0
- package/dist/index.js +8577 -5858
- package/dist/memory/schema.d.ts +3 -3
- package/dist/memory/scoring.d.ts +18 -0
- package/dist/memory/sqlite-provider.d.ts +10 -0
- package/dist/plan/manager.d.ts +10 -0
- package/dist/state.d.ts +16 -0
- package/dist/tools/epic-plan-waves.d.ts +79 -0
- package/dist/tools/epic-record-divergence.d.ts +73 -0
- package/dist/tools/epic-run-phase.d.ts +179 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/manifest.d.ts +3 -0
- package/dist/tools/tool-metadata.d.ts +12 -0
- package/dist/turbo/epic/activation.d.ts +193 -0
- package/dist/turbo/epic/calibration-engine.d.ts +88 -0
- package/dist/turbo/epic/calibration.d.ts +65 -0
- package/dist/turbo/epic/cochange-conflict.d.ts +79 -0
- package/dist/turbo/epic/cochange-source.d.ts +80 -0
- package/dist/turbo/epic/coupling-report.d.ts +85 -0
- package/dist/turbo/epic/divergence-recorder.d.ts +112 -0
- package/dist/turbo/epic/index.d.ts +24 -0
- package/dist/turbo/epic/promotion-evidence.d.ts +42 -0
- package/dist/turbo/epic/state.d.ts +85 -0
- package/dist/turbo/epic/task-commit.d.ts +110 -0
- package/dist/turbo/epic/upstream-commits.d.ts +82 -0
- package/dist/turbo/epic/wave-planner.d.ts +83 -0
- package/dist/turbo/lean/partition-common.d.ts +85 -0
- package/dist/turbo/lean/planner.d.ts +12 -20
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/logger.d.ts +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divergence recorder for Epic Mode Capability D (self-calibration).
|
|
3
|
+
*
|
|
4
|
+
* After every task transitions to `completed`, this module:
|
|
5
|
+
* 1. Compares the task's DECLARED scope (from
|
|
6
|
+
* `.swarm/scopes/scope-{taskId}.json` — what the coder said it would
|
|
7
|
+
* touch) against the ACTUAL files modified during the task
|
|
8
|
+
* (`session.modifiedFilesThisCoderTask` — what the guardrails hook
|
|
9
|
+
* observed the coder writing to).
|
|
10
|
+
* 2. Computes divergence — undeclared writes (actual − declared), unused
|
|
11
|
+
* declarations (declared − actual), and a per-task divergence ratio
|
|
12
|
+
* (undeclared / max(1, actual)).
|
|
13
|
+
* 3. Appends one record to `.swarm/epic/divergence.jsonl`.
|
|
14
|
+
*
|
|
15
|
+
* The calibration engine (`./calibration-engine.ts`) reads this history on
|
|
16
|
+
* the next `epic_decide_phase` invocation and uses it to adjust the
|
|
17
|
+
* activation threshold and hot-module list. This module just records.
|
|
18
|
+
*
|
|
19
|
+
* Pure I/O: never throws to the caller. Failures are logged and swallowed
|
|
20
|
+
* so the task-completion path is never blocked by an audit write.
|
|
21
|
+
*/
|
|
22
|
+
/** One record per task completion. */
|
|
23
|
+
export interface DivergenceRecord {
|
|
24
|
+
/** ISO 8601. */
|
|
25
|
+
timestamp: string;
|
|
26
|
+
sessionID: string;
|
|
27
|
+
taskId: string;
|
|
28
|
+
/** Phase the task belonged to, when known. */
|
|
29
|
+
phaseNumber?: number;
|
|
30
|
+
/** Normalised paths declared via `declare_scope` or files_touched fallback. */
|
|
31
|
+
declaredScope: string[];
|
|
32
|
+
/** Normalised paths the guardrails hook observed the coder write to. */
|
|
33
|
+
actualFiles: string[];
|
|
34
|
+
/** Files in `actualFiles` not present in `declaredScope`. */
|
|
35
|
+
undeclared: string[];
|
|
36
|
+
/** Files in `declaredScope` not present in `actualFiles`. */
|
|
37
|
+
unused: string[];
|
|
38
|
+
/** undeclared.length / max(1, actualFiles.length). 0 ⇒ fully declared. */
|
|
39
|
+
divergenceRatio: number;
|
|
40
|
+
/** True when divergenceRatio === 0 (no undeclared writes). */
|
|
41
|
+
isClean: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute the divergence between a declared scope and the files actually
|
|
45
|
+
* modified. Pure — no I/O, no side effects. Returns the diff sets plus the
|
|
46
|
+
* ratio used by the calibration engine.
|
|
47
|
+
*
|
|
48
|
+
* Path comparison uses `normalizePath` (POSIX-style, no trailing slash,
|
|
49
|
+
* Windows-lowercased) from Lean Turbo's conflicts module so the comparison
|
|
50
|
+
* is consistent with everything else in the lane planner.
|
|
51
|
+
*/
|
|
52
|
+
export declare function computeDivergence(declaredScope: readonly string[], actualFiles: readonly string[]): {
|
|
53
|
+
declared: string[];
|
|
54
|
+
actual: string[];
|
|
55
|
+
undeclared: string[];
|
|
56
|
+
unused: string[];
|
|
57
|
+
divergenceRatio: number;
|
|
58
|
+
};
|
|
59
|
+
interface RecordTaskDivergenceArgs {
|
|
60
|
+
directory: string;
|
|
61
|
+
sessionID: string;
|
|
62
|
+
taskId: string;
|
|
63
|
+
phaseNumber?: number;
|
|
64
|
+
declaredScope: readonly string[];
|
|
65
|
+
actualFiles: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Append one divergence record to the JSONL audit file.
|
|
69
|
+
*
|
|
70
|
+
* Append-only, line-delimited so partial writes are tolerable (the calibration
|
|
71
|
+
* reader skips malformed lines). Best-effort — never throws to caller:
|
|
72
|
+
* - Directory-creation failure → log and return null.
|
|
73
|
+
* - Append write failure → log and return null.
|
|
74
|
+
* Either keeps the task-completion path moving even if the audit subsystem
|
|
75
|
+
* is broken (audit miss is not a correctness issue; blocking task completion
|
|
76
|
+
* would be).
|
|
77
|
+
*/
|
|
78
|
+
export declare function recordTaskDivergence(args: RecordTaskDivergenceArgs): {
|
|
79
|
+
path: string;
|
|
80
|
+
record: DivergenceRecord;
|
|
81
|
+
} | null;
|
|
82
|
+
export interface ReadDivergenceHistoryOptions {
|
|
83
|
+
/** Read at most this many of the most recent records. */
|
|
84
|
+
limit?: number;
|
|
85
|
+
/** Filter to this session (default: all sessions). */
|
|
86
|
+
sessionID?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Maximum bytes to read from the tail of the file. Defaults to
|
|
89
|
+
* `MAX_TAIL_BYTES` (16 MiB) — large enough to hold thousands of
|
|
90
|
+
* records, small enough to avoid OOMing on a runaway audit log.
|
|
91
|
+
* Pass `Infinity` to disable the bound (callers that truly need the
|
|
92
|
+
* whole history — adversarial review H3).
|
|
93
|
+
*/
|
|
94
|
+
maxBytes?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read divergence records from disk, oldest-to-newest within the read
|
|
98
|
+
* window. Malformed lines (rare — could occur on partial write) are
|
|
99
|
+
* silently skipped — they do not corrupt the well-formed records before or
|
|
100
|
+
* after them. Returns `[]` when the file does not exist.
|
|
101
|
+
*
|
|
102
|
+
* Tail-bounded: by default reads at most the last `MAX_TAIL_BYTES`. When
|
|
103
|
+
* the file is larger, the read starts mid-file and the FIRST encountered
|
|
104
|
+
* line (which is almost certainly a partial record split by the byte
|
|
105
|
+
* boundary) is discarded. This means very old records are not returned by
|
|
106
|
+
* a default-bounded read — the calibration engine consumes the tail
|
|
107
|
+
* incrementally via `processedRecords`, so it never needs the full history
|
|
108
|
+
* in memory at once. For full-history audit reads (tests, ad-hoc tooling),
|
|
109
|
+
* pass `maxBytes: Infinity`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function readDivergenceHistory(directory: string, options?: ReadDivergenceHistoryOptions): DivergenceRecord[];
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic mode — barrel export.
|
|
3
|
+
*
|
|
4
|
+
* Epic mode is a new, additive execution mode that composes Lean Turbo without
|
|
5
|
+
* modifying it. Capabilities:
|
|
6
|
+
* - A: co-change-aware pair conflict (`epicPairConflict`).
|
|
7
|
+
* - B: coupling KPI + decoupling roadmap (`computeCouplingReport`).
|
|
8
|
+
* - C: per-plan activation decision (`decideEpicActivation`).
|
|
9
|
+
*
|
|
10
|
+
* Dependency direction is one-way: `epic` depends on `lean`; `lean` never
|
|
11
|
+
* depends on `epic`. All Lean Turbo files stay byte-for-byte untouched.
|
|
12
|
+
*/
|
|
13
|
+
export type { EpicActivationOptions, EpicActivationRationale, EpicActivationVerdict, } from './activation.js';
|
|
14
|
+
export { decideEpicActivation } from './activation.js';
|
|
15
|
+
export type { CoChangeThreshold, EpicPairVerdict, } from './cochange-conflict.js';
|
|
16
|
+
export { epicPairConflict } from './cochange-conflict.js';
|
|
17
|
+
export type { CoChangeData, GetCoChangePairsOptions, } from './cochange-source.js';
|
|
18
|
+
export { getCoChangeData, getCoChangePairs } from './cochange-source.js';
|
|
19
|
+
export type { ComputeCouplingReportOptions, ConflictingPair, CouplingReport, CouplingTask, ModuleContention, } from './coupling-report.js';
|
|
20
|
+
export { computeCouplingReport, formatCouplingReportMarkdown, } from './coupling-report.js';
|
|
21
|
+
export type { PromotionEvidenceRecord } from './promotion-evidence.js';
|
|
22
|
+
export { appendPromotionEvidence, readPromotionEvidence, } from './promotion-evidence.js';
|
|
23
|
+
export type { EpicLastDecision, EpicPersistedState, EpicSessionState, } from './state.js';
|
|
24
|
+
export { disableEpicMode, emptyPersisted as emptyEpicPersisted, emptySessionState as emptyEpicSessionState, enableEpicMode, isEpicModeActive, isStateUnreadable as isEpicStateUnreadable, loadEpicSessionState, recordEpicDecision, repairStateUnreadable as repairEpicStateUnreadable, resetEpicSession, saveEpicSessionState, } from './state.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promotion-evidence writer for Epic Mode (Capability C).
|
|
3
|
+
*
|
|
4
|
+
* Each activation decision (one per phase per session) appends a single
|
|
5
|
+
* JSON line to `.swarm/evidence/epic-promotions.jsonl`. The file is
|
|
6
|
+
* line-delimited so partial writes are tolerable — readers can skip a
|
|
7
|
+
* truncated trailing line and continue.
|
|
8
|
+
*
|
|
9
|
+
* The write is intentionally append-mode (not atomic-rename) because
|
|
10
|
+
* each line is a self-contained record and the file is monotonically
|
|
11
|
+
* growing. Per AGENTS.md invariant 4, writes go under `ctx.directory`,
|
|
12
|
+
* never `process.cwd()`.
|
|
13
|
+
*/
|
|
14
|
+
import type { EpicActivationVerdict } from './activation.js';
|
|
15
|
+
/** What `appendPromotionEvidence` writes per decision. */
|
|
16
|
+
export interface PromotionEvidenceRecord {
|
|
17
|
+
/** ISO 8601 timestamp of the decision. */
|
|
18
|
+
timestamp: string;
|
|
19
|
+
/** Session that made the decision (so multi-session usage stays auditable). */
|
|
20
|
+
sessionID: string;
|
|
21
|
+
/** Phase the decision applied to (Capability C operates per-plan but each
|
|
22
|
+
* phase invokes the runner; we record per phase for granular telemetry). */
|
|
23
|
+
phase?: number;
|
|
24
|
+
/** The decision and the rationale, copied from the activation verdict. */
|
|
25
|
+
verdict: EpicActivationVerdict;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Append one decision record to `.swarm/evidence/epic-promotions.jsonl`.
|
|
29
|
+
*
|
|
30
|
+
* Returns the absolute path of the file written. On any I/O failure the
|
|
31
|
+
* error is rethrown — the caller decides whether to surface a warning to
|
|
32
|
+
* the user or fail closed. Returns null when the directory itself cannot
|
|
33
|
+
* be created (best-effort fail-soft so a missing `.swarm/` does not break
|
|
34
|
+
* the activation flow's primary verdict).
|
|
35
|
+
*/
|
|
36
|
+
export declare function appendPromotionEvidence(directory: string, record: PromotionEvidenceRecord): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Read all evidence records from the JSONL file. Convenience for
|
|
39
|
+
* `/swarm epic status` and tests. Skips malformed lines (best-effort
|
|
40
|
+
* tolerance for the rare partial-write case).
|
|
41
|
+
*/
|
|
42
|
+
export declare function readPromotionEvidence(directory: string): PromotionEvidenceRecord[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable Epic Mode session state (Capability C).
|
|
3
|
+
*
|
|
4
|
+
* Persists per-session Epic Mode activation state under
|
|
5
|
+
* `<projectRoot>/.swarm/epic-state.json` so toggling survives process
|
|
6
|
+
* restarts. Mirrors the pattern in `src/turbo/lean/state.ts` (atomic
|
|
7
|
+
* `tmp + rename`, per-directory `stateUnreadableMap` for fail-closed
|
|
8
|
+
* semantics, sessions-keyed shape) — without modifying that file.
|
|
9
|
+
*
|
|
10
|
+
* Dependency direction is one-way: this module imports nothing from
|
|
11
|
+
* `src/turbo/lean/`. The shape is parallel but independent.
|
|
12
|
+
*/
|
|
13
|
+
/** Top-level state for a single session. */
|
|
14
|
+
export interface EpicSessionState {
|
|
15
|
+
sessionID: string;
|
|
16
|
+
/** When epic mode was last enabled for this session (ISO 8601). */
|
|
17
|
+
enabledAt?: string;
|
|
18
|
+
/** When epic mode was last disabled for this session (ISO 8601). */
|
|
19
|
+
disabledAt?: string;
|
|
20
|
+
/** Most recent activation decision recorded for this session, if any. */
|
|
21
|
+
lastDecision?: EpicLastDecision;
|
|
22
|
+
/** Whether epic mode is currently active for this session. */
|
|
23
|
+
active: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Minimal snapshot of the last activation decision. */
|
|
26
|
+
export interface EpicLastDecision {
|
|
27
|
+
decidedAt: string;
|
|
28
|
+
phase?: number;
|
|
29
|
+
decision: 'promote' | 'demote';
|
|
30
|
+
p: number;
|
|
31
|
+
blockingReasons: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Persisted shape of `.swarm/epic-state.json`. */
|
|
34
|
+
export interface EpicPersistedState {
|
|
35
|
+
version: 1;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
sessions: Record<string, EpicSessionState>;
|
|
38
|
+
}
|
|
39
|
+
export declare function emptyPersisted(): EpicPersistedState;
|
|
40
|
+
export declare function emptySessionState(sessionID: string): EpicSessionState;
|
|
41
|
+
export declare function isStateUnreadable(directory: string): boolean;
|
|
42
|
+
export declare function repairStateUnreadable(directory: string): void;
|
|
43
|
+
/** Read this session's state, or null if not yet recorded. */
|
|
44
|
+
export declare function loadEpicSessionState(directory: string, sessionID: string): EpicSessionState | null;
|
|
45
|
+
/** Write the given session state, replacing any prior entry for that sessionID. */
|
|
46
|
+
export declare function saveEpicSessionState(directory: string, state: EpicSessionState): void;
|
|
47
|
+
/** True iff epic mode is currently active for the given session. */
|
|
48
|
+
export declare function isEpicModeActive(directory: string, sessionID: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* True iff epic mode is currently active for ANY session in the project.
|
|
51
|
+
*
|
|
52
|
+
* Use this when a code path needs to know "is the project running under
|
|
53
|
+
* Epic Mode right now" without caring which session toggled it. The
|
|
54
|
+
* session-scoped `isEpicModeActive` answers "did THIS session toggle it" —
|
|
55
|
+
* a different question with a different answer.
|
|
56
|
+
*
|
|
57
|
+
* The architect's session enables Epic via `/swarm epic on`; sub-agents
|
|
58
|
+
* (coders, reviewers) dispatched through the `Task` tool run in their own
|
|
59
|
+
* sessions and have no record of that toggle. Asking the project-scoped
|
|
60
|
+
* check is the only correct way to honor Epic Mode from those flows.
|
|
61
|
+
* Rule 2's auto-commit (centralized in Phase 5) is the canonical caller.
|
|
62
|
+
*
|
|
63
|
+
* Fail-closed: returns `false` on unreadable state, matching the rest of
|
|
64
|
+
* this module's defaults.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isEpicModeActiveForProject(directory: string): boolean;
|
|
67
|
+
/** Enable epic mode for the session; records `enabledAt`. */
|
|
68
|
+
export declare function enableEpicMode(directory: string, sessionID: string): void;
|
|
69
|
+
/** Disable epic mode for the session; records `disabledAt`. */
|
|
70
|
+
export declare function disableEpicMode(directory: string, sessionID: string): void;
|
|
71
|
+
/** Reset the session's state entry entirely. */
|
|
72
|
+
export declare function resetEpicSession(directory: string, sessionID: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Update the session's `lastDecision` field. Used by the runner after each
|
|
75
|
+
* activation evaluation so `/swarm epic status` can show the most recent
|
|
76
|
+
* decision rationale without re-reading the evidence JSONL.
|
|
77
|
+
*
|
|
78
|
+
* Precondition: the session must already have an entry (i.e. the caller has
|
|
79
|
+
* called `enableEpicMode` previously). This is intentional — recording a
|
|
80
|
+
* decision for a never-toggled session would produce phantom state that
|
|
81
|
+
* `/swarm epic status` could not distinguish from a legitimately-active
|
|
82
|
+
* session. Callers that reach this function should have already verified
|
|
83
|
+
* `isEpicModeActive(...)` returned `true`. Throws if no session entry exists.
|
|
84
|
+
*/
|
|
85
|
+
export declare function recordEpicDecision(directory: string, sessionID: string, decision: EpicLastDecision): void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-commit on task completion — Rule 2 of the greenfield-smart redesign.
|
|
3
|
+
*
|
|
4
|
+
* When Epic Mode is active for the session and the project is a git repo,
|
|
5
|
+
* `update_task_status` calls `commitTaskCompletion` after a task transitions
|
|
6
|
+
* to `completed` and the durable plan write has succeeded. The resulting
|
|
7
|
+
* commit serves two purposes:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Greenfield gate progress.** Each completed-and-committed task
|
|
10
|
+
* advances `commitsObserved` so the activation gate
|
|
11
|
+
* (`src/turbo/epic/activation.ts`) eventually opens. Without this,
|
|
12
|
+
* an Epic-only workflow never produces commits and the gate
|
|
13
|
+
* permanently blocks parallel promotion — the exact failure mode
|
|
14
|
+
* Rule 4 of the redesign identified.
|
|
15
|
+
*
|
|
16
|
+
* 2. **Parallel-eligibility evidence (Rule 3).** Downstream tasks can
|
|
17
|
+
* require their `depends:` upstream to be *committed* (not just
|
|
18
|
+
* marked complete) before they fan out. The commit message format
|
|
19
|
+
* `swarm(task <id>): ...` is the searchable marker the lane
|
|
20
|
+
* planner consumes in `upstream-commits.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Failure handling: every step degrades non-fatally. A failed commit must
|
|
23
|
+
* never block the durable task-status update — the plan ledger is the
|
|
24
|
+
* authoritative source (AGENTS.md #5), git is a downstream artifact.
|
|
25
|
+
*
|
|
26
|
+
* Subprocess discipline: delegates to `src/git/branch.ts`, which already
|
|
27
|
+
* enforces AGENTS.md #3 (explicit cwd, bounded timeout, array-form spawn,
|
|
28
|
+
* non-interactive). This module adds no new subprocess primitives.
|
|
29
|
+
*/
|
|
30
|
+
/** Result of a single task-commit attempt. */
|
|
31
|
+
export interface CommitTaskCompletionResult {
|
|
32
|
+
/**
|
|
33
|
+
* `true` when a `swarm(task <id>):` marker for this taskId is present
|
|
34
|
+
* in git history at function exit — whether this call produced it
|
|
35
|
+
* (`reason: 'success'`) or whether an earlier call did
|
|
36
|
+
* (`reason: 'idempotent-skip'`).
|
|
37
|
+
*
|
|
38
|
+
* Phase 17 (B.M9): pre-Phase-17 the `'already-committed'` reason
|
|
39
|
+
* returned `committed: false`, self-contradicting ("not committed
|
|
40
|
+
* because already committed"). Architect LLMs interpreted the
|
|
41
|
+
* `false` as a failure and retried, producing log noise. The fixed
|
|
42
|
+
* semantic: `committed` answers "is the marker in git for this
|
|
43
|
+
* taskId now?" — yes for both the fresh-write and the idempotent
|
|
44
|
+
* skip paths.
|
|
45
|
+
*/
|
|
46
|
+
committed: boolean;
|
|
47
|
+
reason: 'no-git' | 'commit-failed' | 'success' | 'idempotent-skip';
|
|
48
|
+
sha?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function formatTaskCommitMessage(taskId: string, description?: string): string;
|
|
52
|
+
export declare function commitTaskCompletion(directory: string, taskId: string, description?: string, scopePaths?: string[]): Promise<CommitTaskCompletionResult>;
|
|
53
|
+
/**
|
|
54
|
+
* DI seam — production code calls through `_internals.<name>` so tests
|
|
55
|
+
* substitute deterministic doubles without `mock.module`'s cross-file
|
|
56
|
+
* leak (AGENTS.md invariant 7). Restore in `afterEach`.
|
|
57
|
+
*/
|
|
58
|
+
export declare const _internals: {
|
|
59
|
+
isGitRepo: (cwd: string) => boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Stage exactly the declared scope paths for this task. The trailing
|
|
62
|
+
* `:(exclude).swarm` + `:(exclude).swarm/**` pathspecs are belt-and-
|
|
63
|
+
* suspenders against AGENTS.md #4 even when the architect's scope
|
|
64
|
+
* declaration accidentally points into `.swarm/`. We do NOT rely on
|
|
65
|
+
* the user's `.gitignore`: a single misconfigured project would
|
|
66
|
+
* otherwise commit prompts, ledgers, telemetry, and evidence into
|
|
67
|
+
* git history every time Rule 2 fires.
|
|
68
|
+
*
|
|
69
|
+
* Missing pathspecs (declared scope points to a non-existent file) are
|
|
70
|
+
* left to surface as a `commit-failed` reason — better than silent
|
|
71
|
+
* staging skip, since it tells the user their scope declaration is
|
|
72
|
+
* stale. Rule 2's non-fatal contract means the plan-write still wins.
|
|
73
|
+
*/
|
|
74
|
+
stageScopedPaths: (cwd: string, paths: string[]) => void;
|
|
75
|
+
/**
|
|
76
|
+
* `--allow-empty` variant of commit. We don't expose this in
|
|
77
|
+
* `src/git/branch.ts` because it's specific to the task-completion
|
|
78
|
+
* marker semantics — a normal commit should fail on empty trees to
|
|
79
|
+
* surface bugs. Here we explicitly want the marker.
|
|
80
|
+
*
|
|
81
|
+
* Phase 8: `--no-verify` skips `pre-commit`, `commit-msg`, and
|
|
82
|
+
* `pre-commit-msg` hooks. Rule 2's commits are protocol markers, not
|
|
83
|
+
* user-authored content — running Biome/typecheck/lint on every task
|
|
84
|
+
* completion would add minutes of wall-clock per task and, worse,
|
|
85
|
+
* could block the marker entirely on a repo with a strict pre-commit
|
|
86
|
+
* gate. Plan ledger remains authoritative; the commit is the audit
|
|
87
|
+
* trail, not the gate.
|
|
88
|
+
*/
|
|
89
|
+
commitAllowEmpty: (cwd: string, message: string) => void;
|
|
90
|
+
gitHeadSha: (cwd: string) => string;
|
|
91
|
+
/**
|
|
92
|
+
* Returns true when a `swarm(task <id>):` marker subject for this
|
|
93
|
+
* taskId already exists anywhere in git history. Used by the
|
|
94
|
+
* idempotency guard above so repeat completion calls don't mint
|
|
95
|
+
* duplicate markers.
|
|
96
|
+
*
|
|
97
|
+
* Implementation: `git log --grep=<pattern> -F` is NOT used (it
|
|
98
|
+
* fixed-string-matches the whole subject); instead we anchor the
|
|
99
|
+
* regex with `--extended-regexp` and bound the scan with `-n 1` so a
|
|
100
|
+
* single match suffices. Returns false on any git failure — the
|
|
101
|
+
* caller treats that as "unknown" and proceeds.
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
* Phase 11 (B5): async sleep used by `commitTaskCompletion`'s
|
|
105
|
+
* retry loop. Routed through `_internals` so tests can substitute
|
|
106
|
+
* a no-op stub and not actually wait during fast-path unit tests.
|
|
107
|
+
*/
|
|
108
|
+
sleep: (ms: number) => Promise<void>;
|
|
109
|
+
hasExistingTaskCommit: (cwd: string, taskId: string) => boolean;
|
|
110
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream-commit predicate — Rule 3 of the greenfield-smart redesign.
|
|
3
|
+
*
|
|
4
|
+
* Given a project directory, returns a fast `(taskId) => boolean` predicate
|
|
5
|
+
* that answers "has this task been committed?" The lane planner consults
|
|
6
|
+
* this when evaluating cross-batch dependencies: a downstream task is
|
|
7
|
+
* parallel-eligible only when every `depends:` upstream that lives in a
|
|
8
|
+
* prior phase batch is already in git HEAD's history.
|
|
9
|
+
*
|
|
10
|
+
* Why this matters: without it, the planner treats any dep not in the
|
|
11
|
+
* current task batch as implicitly satisfied (the existing behavior in
|
|
12
|
+
* `src/turbo/lean/planner.ts:381-390`). That's fine when the prior batch
|
|
13
|
+
* actually finished cleanly, but it doesn't distinguish "marked complete"
|
|
14
|
+
* from "marked complete *and* the work is in version control". Rule 3
|
|
15
|
+
* insists on the stronger condition so parallel coders can't inherit an
|
|
16
|
+
* uncommitted upstream worktree.
|
|
17
|
+
*
|
|
18
|
+
* Source format: `commitTaskCompletion` writes commit subjects shaped like
|
|
19
|
+
* `swarm(task <id>): <description>` (see `./task-commit.ts:formatTaskCommitMessage`).
|
|
20
|
+
* This module parses the `<id>` from those subjects.
|
|
21
|
+
*
|
|
22
|
+
* Boundedness (AGENTS.md #3): subprocess timeout matches the rest of git
|
|
23
|
+
* helpers (30s); `--max-count` caps the log scan; failures degrade to a
|
|
24
|
+
* permissive predicate so the planner does not regress when git is broken.
|
|
25
|
+
*/
|
|
26
|
+
export interface BuildUpstreamCommitsOptions {
|
|
27
|
+
/** Override the log scan window. Default: 10,000. */
|
|
28
|
+
maxCommits?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Eagerly read git log once and return a fast `(taskId) => boolean`
|
|
32
|
+
* predicate the lane planner uses for cross-batch upstream-commit checks.
|
|
33
|
+
*
|
|
34
|
+
* Single evidence source: a `swarm(task <id>):` commit subject in git
|
|
35
|
+
* log, produced by Rule 2's `commitTaskCompletion`.
|
|
36
|
+
*
|
|
37
|
+
* History note: an earlier revision OR'd in a plan-ledger fallback (any
|
|
38
|
+
* task with `status: completed` in `.swarm/plan.json` was treated as
|
|
39
|
+
* committed) to guard against a hypothetical deadlock where `commit-
|
|
40
|
+
* TaskCompletion` failed silently. Phase 5 of the 2026-06-03 corrective
|
|
41
|
+
* plan made Rule 2 reliable on every completion path by centralizing
|
|
42
|
+
* the invocation in `plan/manager.updateTaskStatus` — so the guard's
|
|
43
|
+
* premise no longer holds, and keeping the fallback would defeat Rule 3's
|
|
44
|
+
* own purpose (distinguishing "marked complete" from "in git history").
|
|
45
|
+
* Removed in Phase 6.
|
|
46
|
+
*
|
|
47
|
+
* Failure mode:
|
|
48
|
+
* - Git log read fails (no git, spawn error, timeout) → predicate
|
|
49
|
+
* returns `true` for everything (permissive). The lane planner falls
|
|
50
|
+
* back to its legacy "cross-batch dep implicitly satisfied" behavior,
|
|
51
|
+
* which is the pre-Rule-3 semantics. Better legacy than wedged in a
|
|
52
|
+
* broken environment.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildIsUpstreamCommitted(directory: string, options?: BuildUpstreamCommitsOptions): (taskId: string) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Phase 12 (B10) — same predicate construction as `buildIsUpstreamCommitted`
|
|
57
|
+
* but exposes whether the git-log read failed. Callers that need to FAIL
|
|
58
|
+
* CLOSED on a broken git environment (e.g. the Phase 10 activation gate,
|
|
59
|
+
* where the predicate is the only safety signal) use `gitFailed` to pick
|
|
60
|
+
* a stricter policy; callers that can tolerate "I don't know" fall-open
|
|
61
|
+
* (e.g. Rule 3 at the lane planner, where wave ordering is the backstop)
|
|
62
|
+
* continue using `buildIsUpstreamCommitted` directly.
|
|
63
|
+
*
|
|
64
|
+
* The two-tier API exists because the original permissive degradation
|
|
65
|
+
* was correct for Rule 3 (no regression vs pre-Rule-3 semantics) but
|
|
66
|
+
* inverted the safety polarity for Phase 10 (which has no Path-B
|
|
67
|
+
* fallback after the commit-count floor was retired).
|
|
68
|
+
*/
|
|
69
|
+
export interface UpstreamCommittedEvidence {
|
|
70
|
+
predicate: (taskId: string) => boolean;
|
|
71
|
+
/** True when the git-log read threw — predicate is the permissive fallback. */
|
|
72
|
+
gitFailed: boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare function buildIsUpstreamCommittedWithStatus(directory: string, options?: BuildUpstreamCommitsOptions): UpstreamCommittedEvidence;
|
|
75
|
+
/**
|
|
76
|
+
* DI seam — production code routes the git-log read through `_internals`
|
|
77
|
+
* so tests can substitute deterministic doubles without `mock.module`
|
|
78
|
+
* (AGENTS.md invariant 7).
|
|
79
|
+
*/
|
|
80
|
+
export declare const _internals: {
|
|
81
|
+
readGitLogSubjects: (cwd: string, max: number) => string;
|
|
82
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic Mode Wave Planner.
|
|
3
|
+
*
|
|
4
|
+
* The wave planner partitions a phase's pending tasks into ordered *waves*.
|
|
5
|
+
* A wave is a set of tasks that:
|
|
6
|
+
* - have all their dependencies satisfied by tasks in completed waves, AND
|
|
7
|
+
* - have mutually disjoint declared scopes (no two tasks in the same wave
|
|
8
|
+
* touch a shared file or parent/child path).
|
|
9
|
+
*
|
|
10
|
+
* Architectural contrast with the lane planner:
|
|
11
|
+
* - Lane planner: greedy fill into a fixed number of independent serial
|
|
12
|
+
* chains. Branching DAGs (e.g. `A → B → {C, D, E}`) collapse into a
|
|
13
|
+
* single chain because the cross-lane-dep rule forbids C/D/E from
|
|
14
|
+
* living in a different lane than A/B.
|
|
15
|
+
* - Wave planner: emits a sequence of concurrent groups. The same DAG
|
|
16
|
+
* becomes `wave 1: [A], wave 2: [B], wave 3: [C, D, E]`. Within a wave
|
|
17
|
+
* the architect dispatches one Task per taskId — all in one message —
|
|
18
|
+
* and the next wave starts only when the prior one is done.
|
|
19
|
+
*
|
|
20
|
+
* The two planners share `runPartitionPreflight` from `partition-common.ts`
|
|
21
|
+
* so classification (global/protected/no-scope/normal), scope resolution,
|
|
22
|
+
* and topological sort are identical. Divergence is intentional and lives
|
|
23
|
+
* only in the assignment loop below.
|
|
24
|
+
*/
|
|
25
|
+
import type { LeanTurboConfig } from '../../config/schema';
|
|
26
|
+
import { type PlanPhase } from '../lean/partition-common';
|
|
27
|
+
import type { LeanTurboDegradedTask } from '../lean/state';
|
|
28
|
+
/**
|
|
29
|
+
* A single wave in the Epic plan. Tasks in a wave run concurrently; the next
|
|
30
|
+
* wave starts only after all tasks in this wave complete.
|
|
31
|
+
*/
|
|
32
|
+
export interface EpicWave {
|
|
33
|
+
/** 1-indexed wave number (display + ordering). */
|
|
34
|
+
waveId: number;
|
|
35
|
+
/** Tasks in this wave. Architect dispatches one Task per id, all in one message. */
|
|
36
|
+
taskIds: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Union of declared scope files across the wave. Informational — Epic
|
|
39
|
+
* Mode does NOT acquire file locks (architect-driven dispatch goes
|
|
40
|
+
* directly through `Task`, bypassing the lean runner's lock path).
|
|
41
|
+
* Used for evidence/divergence reporting.
|
|
42
|
+
*/
|
|
43
|
+
files: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The complete wave plan produced by `planEpicWaves`.
|
|
47
|
+
*/
|
|
48
|
+
export interface EpicWavePlan {
|
|
49
|
+
/** The phase number this plan covers. */
|
|
50
|
+
phase: number;
|
|
51
|
+
/** Unique identifier for this wave plan. */
|
|
52
|
+
planId: string;
|
|
53
|
+
/** Ordered list of waves. Empty when every task degraded or serialized. */
|
|
54
|
+
waves: EpicWave[];
|
|
55
|
+
/** Tasks that were serialized (cycles, no-scope, invalid-scope, protected with serialize policy). */
|
|
56
|
+
serializedTasks: string[];
|
|
57
|
+
/** Tasks that were degraded (global files, protected paths, Rule-3 leftovers). */
|
|
58
|
+
degradedTasks: LeanTurboDegradedTask[];
|
|
59
|
+
/** Human-readable summary when all tasks ended up degraded. */
|
|
60
|
+
degradationSummary?: string;
|
|
61
|
+
/** Total number of pending tasks the planner saw (before assignment). */
|
|
62
|
+
totalPendingTasks: number;
|
|
63
|
+
/** Sum of `waves[*].taskIds.length` — tasks that will actually run concurrently in a wave. */
|
|
64
|
+
totalConcurrentTasks: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Partition phase tasks into ordered concurrent waves.
|
|
68
|
+
*
|
|
69
|
+
* @param directory - Project root directory
|
|
70
|
+
* @param phaseNumber - Phase number to plan
|
|
71
|
+
* @param plan - The full plan object (from `.swarm/plan.json`)
|
|
72
|
+
* @param config - Lean Turbo configuration (reused for risk/conflict policy)
|
|
73
|
+
* @param scopes - Optional pre-loaded scopes map (taskId -> file paths)
|
|
74
|
+
* @param isUpstreamCommitted - Optional Rule-3 predicate (greenfield-smart).
|
|
75
|
+
* When supplied, a cross-batch dependency (a `depends:` upstream NOT
|
|
76
|
+
* in this planning call's task set) is treated as satisfied only if
|
|
77
|
+
* the predicate returns `true`. Without it, legacy semantics apply
|
|
78
|
+
* (cross-batch deps implicitly satisfied).
|
|
79
|
+
* @returns Complete wave plan with ordered concurrent groups.
|
|
80
|
+
*/
|
|
81
|
+
export declare function planEpicWaves(directory: string, phaseNumber: number, plan: {
|
|
82
|
+
phases: PlanPhase[];
|
|
83
|
+
}, config: LeanTurboConfig, scopes?: Record<string, string[]>, isUpstreamCommitted?: (taskId: string) => boolean): EpicWavePlan;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared partition primitives for Lean Turbo's lane planner AND Epic Mode's
|
|
3
|
+
* wave planner. Both planners run the same three preflight steps:
|
|
4
|
+
*
|
|
5
|
+
* 1. Resolve declared scopes (or files_touched fallback) per pending task.
|
|
6
|
+
* 2. Classify each task by file risk (global / protected / no-scope / normal).
|
|
7
|
+
* 3. Topologically sort with cycle detection so a downstream task can never
|
|
8
|
+
* be released before its upstream.
|
|
9
|
+
*
|
|
10
|
+
* They diverge only at the assignment step:
|
|
11
|
+
* - Lane planner: greedy fill into a fixed number of serial chains.
|
|
12
|
+
* - Wave planner: emit ordered concurrent groups whose membership is gated
|
|
13
|
+
* by inter-task scope disjointness.
|
|
14
|
+
*
|
|
15
|
+
* Owning the preflight in one place guarantees both planners produce
|
|
16
|
+
* identical classifications and sort orders for the same inputs.
|
|
17
|
+
*/
|
|
18
|
+
import type { LeanTurboConfig } from '../../config/schema';
|
|
19
|
+
import { type TaskRiskAssessment } from './risk';
|
|
20
|
+
/**
|
|
21
|
+
* A single task within a plan phase. Matches `.swarm/plan.json`.
|
|
22
|
+
*/
|
|
23
|
+
export interface PlanTask {
|
|
24
|
+
id: string;
|
|
25
|
+
description: string;
|
|
26
|
+
status: 'pending' | 'in_progress' | 'completed' | 'blocked';
|
|
27
|
+
depends?: string[];
|
|
28
|
+
files_touched?: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A phase within a plan, containing multiple tasks.
|
|
32
|
+
*/
|
|
33
|
+
export interface PlanPhase {
|
|
34
|
+
id: number;
|
|
35
|
+
name: string;
|
|
36
|
+
tasks: PlanTask[];
|
|
37
|
+
}
|
|
38
|
+
export type ClassifiedTask = {
|
|
39
|
+
task: PlanTask;
|
|
40
|
+
files: string[];
|
|
41
|
+
hasDeclaredScope: boolean;
|
|
42
|
+
category: TaskRiskAssessment['category'];
|
|
43
|
+
conflictReason?: string;
|
|
44
|
+
};
|
|
45
|
+
export interface PartitionPreflight {
|
|
46
|
+
/** Topologically sorted, lexicographically tie-broken. */
|
|
47
|
+
sortedTasks: ClassifiedTask[];
|
|
48
|
+
/** taskId -> ClassifiedTask for O(1) lookup. */
|
|
49
|
+
taskMap: Map<string, ClassifiedTask>;
|
|
50
|
+
/** Tasks whose deps form a cycle. Callers must fail-closed: serialize them. */
|
|
51
|
+
tasksInCycle: Set<string>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate and normalize a task's declared scope.
|
|
55
|
+
*
|
|
56
|
+
* Symlink containment is NOT enforced here — the lock layer resolves symlinks
|
|
57
|
+
* at acquisition time. This lets architects declare scopes with symlinks for
|
|
58
|
+
* convenience without compromising actual file-write safety.
|
|
59
|
+
*
|
|
60
|
+
* @returns Tuple of [validFiles, invalidCount]
|
|
61
|
+
*/
|
|
62
|
+
export declare function getValidatedFiles(files: string[], directory: string): [string[], number];
|
|
63
|
+
/**
|
|
64
|
+
* Run the shared preflight: resolve scopes, classify by risk, topo-sort with
|
|
65
|
+
* cycle detection.
|
|
66
|
+
*
|
|
67
|
+
* The same `(directory, pendingTasks, config, scopes)` produces the same
|
|
68
|
+
* `PartitionPreflight` from both planners.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runPartitionPreflight(directory: string, pendingTasks: PlanTask[], config: LeanTurboConfig, scopes?: Record<string, string[]>): PartitionPreflight;
|
|
71
|
+
/**
|
|
72
|
+
* Build the predicate that a task's dependencies are all satisfied.
|
|
73
|
+
*
|
|
74
|
+
* Rule 3 (greenfield-smart): when `isUpstreamCommitted` is supplied, a
|
|
75
|
+
* cross-batch dependency (a `depends:` upstream NOT in the current planning
|
|
76
|
+
* call's task set — typically completed in a prior phase) is treated as
|
|
77
|
+
* satisfied **only** if the predicate returns `true`. Without the predicate
|
|
78
|
+
* the legacy semantics apply: cross-batch deps are implicitly satisfied.
|
|
79
|
+
*/
|
|
80
|
+
export declare function makeDependencySatisfactionChecker(taskMap: Map<string, ClassifiedTask>, assignedTasks: Set<string>, isUpstreamCommitted?: (taskId: string) => boolean): (task: ClassifiedTask) => boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Return the next group of tasks ready for assignment: not yet assigned and
|
|
83
|
+
* all dependencies satisfied. Lexicographically sorted for determinism.
|
|
84
|
+
*/
|
|
85
|
+
export declare function getReadyTasks(sortedTasks: ClassifiedTask[], assignedTasks: Set<string>, isSatisfied: (task: ClassifiedTask) => boolean): ClassifiedTask[];
|