@cortexkit/opencode-magic-context 0.33.0 → 0.34.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/dist/config/agent-disable.d.ts +22 -0
- package/dist/config/agent-disable.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +7 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +144 -24
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config/transform-mode.d.ts +2 -0
- package/dist/config/transform-mode.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-bwa99a04.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
- package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
- package/dist/features/builtin-commands/commands.d.ts +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +106 -0
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +0 -2
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +0 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/index.d.ts +2 -2
- package/dist/features/magic-context/git-commits/index.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts +0 -2
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts +0 -13
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts +1 -2
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts +0 -6
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +0 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts +1 -6
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts +0 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
- package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
- package/dist/features/magic-context/mural/storage-mural.d.ts +0 -1
- package/dist/features/magic-context/mural/storage-mural.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +8 -1
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +16 -2
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
- package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/capabilities.d.ts +0 -2
- package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +0 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/types.d.ts +0 -16
- package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +35 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -5
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +0 -2
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/types.d.ts +0 -3
- package/dist/hooks/auto-update-checker/types.d.ts.map +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
- package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts +2 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts +49 -0
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
- package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts +12 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +0 -12
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
- package/dist/hooks/magic-context/event-handler.d.ts +7 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +11 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +18 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +39 -0
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +19 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +27 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-kxz4tn6m.js → index-56yjaeb2.js} +56 -4
- package/dist/{index-bmkea2dq.js → index-b563sga5.js} +40 -287
- package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
- package/dist/{index-k5y60ymm.js → index-dtas91s5.js} +27 -6
- package/dist/index-fcyvznch.js +279 -0
- package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
- package/dist/{index-h86twgsp.js → index-s0mdn690.js} +2113 -1067
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1356 -627
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +19 -8
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -0
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +3 -7
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts +8 -0
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-yh7pepvx.js → recomp-orchestrator-8e82vmvb.js} +6 -5
- package/dist/send-session-notification-6njqewzk.js +20 -0
- package/dist/shared/conflict-detector.d.ts +38 -1
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/conflict-fixer.d.ts +18 -1
- package/dist/shared/conflict-fixer.d.ts.map +1 -1
- package/dist/shared/context-limit-provenance.d.ts +3 -0
- package/dist/shared/context-limit-provenance.d.ts.map +1 -0
- package/dist/shared/escalation-bands.d.ts +9 -0
- package/dist/shared/escalation-bands.d.ts.map +1 -0
- package/dist/shared/format-threshold.d.ts +1 -1
- package/dist/shared/models-dev-cache.d.ts +29 -3
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +46 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +9 -4
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/storage-permissions.d.ts +5 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -0
- package/dist/{storage-meta-session-1phy0b1z.js → storage-meta-session-xt41zbw4.js} +2 -1
- package/dist/{task-config-dn9k9szr.js → task-config-5nx3qfz8.js} +1 -2
- package/dist/tui/compaction-off.d.ts +13 -0
- package/dist/tui/compaction-off.d.ts.map +1 -0
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/compaction-off.d.ts +13 -0
- package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +8 -3
- package/src/agents/dreamer.ts +68 -0
- package/src/agents/hidden-agent-registrations.ts +331 -0
- package/src/agents/historian.ts +3 -0
- package/src/agents/language-directive.test.ts +138 -0
- package/src/agents/language-directive.ts +121 -0
- package/src/agents/magic-context-prompt.language.test.ts +35 -0
- package/src/agents/magic-context-prompt.test.ts +292 -0
- package/src/agents/magic-context-prompt.ts +182 -0
- package/src/agents/permissions.test.ts +252 -0
- package/src/agents/permissions.ts +186 -0
- package/src/agents/sidekick.ts +1 -0
- package/src/agents/smart-note-compiler.ts +1 -0
- package/src/config/agent-disable.ts +106 -0
- package/src/config/compaction-accessor-guard.test.ts +82 -0
- package/src/config/compaction-backcompat.test.ts +52 -0
- package/src/config/index.test.ts +904 -0
- package/src/config/index.ts +601 -0
- package/src/config/migrate-config-location.test.ts +332 -0
- package/src/config/migrate-config-location.ts +635 -0
- package/src/config/migrate-dreamer-v2.test.ts +287 -0
- package/src/config/migrate-dreamer-v2.ts +336 -0
- package/src/config/migrate-experimental.ts +159 -0
- package/src/config/project-security.test.ts +366 -0
- package/src/config/project-security.ts +588 -0
- package/src/config/prune-config-leaf.test.ts +78 -0
- package/src/config/prune-config-leaf.ts +68 -0
- package/src/config/removed-config-keys.test.ts +116 -0
- package/src/config/schema/agent-overrides.ts +45 -0
- package/src/config/schema/magic-context.test.ts +358 -0
- package/src/config/schema/magic-context.ts +1002 -0
- package/src/config/transform-mode.test.ts +62 -0
- package/src/config/transform-mode.ts +34 -0
- package/src/config/variable.test.ts +323 -0
- package/src/config/variable.ts +192 -0
- package/src/features/magic-context/defaults.ts +1 -0
- package/src/features/magic-context/dreamer/cron.ts +243 -0
- package/src/features/magic-context/dreamer/task-registry.ts +104 -0
- package/src/shared/conflict-detector.test.ts +251 -1
- package/src/shared/conflict-detector.ts +84 -5
- package/src/shared/conflict-fixer.test.ts +309 -0
- package/src/shared/conflict-fixer.ts +82 -5
- package/src/shared/context-limit-provenance.ts +6 -0
- package/src/shared/escalation-bands.test.ts +23 -0
- package/src/shared/escalation-bands.ts +18 -0
- package/src/shared/format-threshold.ts +1 -1
- package/src/shared/models-dev-cache.test.ts +171 -0
- package/src/shared/models-dev-cache.ts +195 -38
- package/src/shared/rpc-server.ts +27 -18
- package/src/shared/rpc-types.ts +46 -1
- package/src/shared/sqlite.ts +13 -33
- package/src/shared/storage-permissions.ts +20 -0
- package/src/tui/compaction-off.test.ts +56 -0
- package/src/tui/compaction-off.ts +29 -0
- package/src/tui/data/context-db.ts +6 -0
- package/src/tui/index.tsx +113 -65
- package/src/tui/slots/sidebar-content.tsx +139 -83
- package/src/tui-compiled/compaction-off.ts +29 -0
- package/src/tui-compiled/data/context-db.ts +6 -0
- package/src/tui-compiled/index.tsx +558 -455
- package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
- package/dist/config/schema.d.ts +0 -3
- package/dist/config/schema.d.ts.map +0 -1
- package/dist/features/magic-context/plugin-messages.d.ts +0 -75
- package/dist/features/magic-context/plugin-messages.d.ts.map +0 -1
- package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts +0 -21
- package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts.map +0 -1
- package/dist/index-anhrfygk.js +0 -57
- package/dist/send-session-notification-tghq9gnh.js +0 -11
- package/dist/shared/opencode-compaction-detector.d.ts +0 -2
- package/dist/shared/opencode-compaction-detector.d.ts.map +0 -1
- package/dist/shared/transcript-opencode.d.ts +0 -71
- package/dist/shared/transcript-opencode.d.ts.map +0 -1
- package/dist/tools/index.d.ts +0 -6
- package/dist/tools/index.d.ts.map +0 -1
- package/src/shared/opencode-compaction-detector.test.ts +0 -239
- package/src/shared/opencode-compaction-detector.ts +0 -81
- package/src/shared/transcript-opencode.ts +0 -302
- /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal 5-field cron evaluator for per-task dreamer scheduling.
|
|
3
|
+
*
|
|
4
|
+
* Why vendored (no dependency): we only need "next occurrence after time T", the
|
|
5
|
+
* 3-day package-release-age floor makes pulling a dep awkward, and the surface is
|
|
6
|
+
* small. Shared core — both OpenCode and Pi import this via @magic-context/core.
|
|
7
|
+
*
|
|
8
|
+
* Fields: `minute hour day-of-month month day-of-week`
|
|
9
|
+
* minute 0-59 · hour 0-23 · dom 1-31 · month 1-12 · dow 0-6 (0 or 7 = Sunday)
|
|
10
|
+
* Each field supports: a star wildcard, a star-with-step (every-N), a single
|
|
11
|
+
* value `a`, a range `a-b`, a range-with-step `a-b/step`, an open step `a/step`
|
|
12
|
+
* (a..max), and comma-lists of any of those. Numeric only — month and weekday
|
|
13
|
+
* NAMES are intentionally unsupported (smaller surface; add later if asked).
|
|
14
|
+
* Empty string `""` means "never" and is handled by the caller, not here.
|
|
15
|
+
*
|
|
16
|
+
* Day matching uses Vixie OR-semantics: when BOTH dom and dow are restricted
|
|
17
|
+
* (not `*`), a day matches if EITHER matches; when only one is restricted, only
|
|
18
|
+
* that one is consulted; when neither is restricted, every day matches.
|
|
19
|
+
*
|
|
20
|
+
* Timezone: all matching is in the machine's LOCAL time — the dreamer's whole
|
|
21
|
+
* purpose is "run while the user is asleep", which is a wall-clock concept.
|
|
22
|
+
* nextOccurrence steps by real (epoch) minutes and reads LOCAL civil fields off
|
|
23
|
+
* each candidate, so DST transitions are handled correctly by construction (no
|
|
24
|
+
* setHours/local-constructor normalization, which is DST-ambiguous).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface ParsedCron {
|
|
28
|
+
minute: Set<number>;
|
|
29
|
+
hour: Set<number>;
|
|
30
|
+
dom: Set<number>;
|
|
31
|
+
month: Set<number>;
|
|
32
|
+
dow: Set<number>;
|
|
33
|
+
/** Original field token was not `*` — drives Vixie dom/dow OR-semantics. */
|
|
34
|
+
domRestricted: boolean;
|
|
35
|
+
dowRestricted: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ParseCronResult = { ok: true; cron: ParsedCron } | { ok: false; error: string };
|
|
39
|
+
|
|
40
|
+
interface FieldSpec {
|
|
41
|
+
name: string;
|
|
42
|
+
min: number;
|
|
43
|
+
max: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FIELDS: FieldSpec[] = [
|
|
47
|
+
{ name: "minute", min: 0, max: 59 },
|
|
48
|
+
{ name: "hour", min: 0, max: 23 },
|
|
49
|
+
{ name: "day-of-month", min: 1, max: 31 },
|
|
50
|
+
{ name: "month", min: 1, max: 12 },
|
|
51
|
+
{ name: "day-of-week", min: 0, max: 7 }, // 7 normalized to 0 (Sunday)
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const MINUTE_MS = 60_000;
|
|
55
|
+
/**
|
|
56
|
+
* Forward search bound for nextOccurrence. ~4 years covers Feb-29-only crons
|
|
57
|
+
* (leap years are ≤4 apart) and the longest legitimate gaps; anything past it is
|
|
58
|
+
* treated as "never" (e.g. an impossible `0 0 31 2 *` — Feb 31). Generous so a
|
|
59
|
+
* valid-but-rare schedule is never wrongly reported as never.
|
|
60
|
+
*/
|
|
61
|
+
const MAX_SEARCH_MS = 4 * 366 * 24 * 60 * MINUTE_MS;
|
|
62
|
+
|
|
63
|
+
/** Parse one field into the set of matching values, or null on error. */
|
|
64
|
+
function parseField(token: string, spec: FieldSpec): Set<number> | null {
|
|
65
|
+
const values = new Set<number>();
|
|
66
|
+
const normalize = (n: number): number => (spec.name === "day-of-week" && n === 7 ? 0 : n);
|
|
67
|
+
|
|
68
|
+
for (const part of token.split(",")) {
|
|
69
|
+
const piece = part.trim();
|
|
70
|
+
if (piece.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
// Split off an optional /step.
|
|
73
|
+
const [rangePart, stepPart, ...extra] = piece.split("/");
|
|
74
|
+
if (extra.length > 0) return null;
|
|
75
|
+
let step = 1;
|
|
76
|
+
if (stepPart !== undefined) {
|
|
77
|
+
if (!/^\d+$/.test(stepPart)) return null;
|
|
78
|
+
step = Number(stepPart);
|
|
79
|
+
if (step < 1) return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let lo: number;
|
|
83
|
+
let hi: number;
|
|
84
|
+
if (rangePart === "*") {
|
|
85
|
+
lo = spec.min;
|
|
86
|
+
hi = spec.max;
|
|
87
|
+
} else if (rangePart.includes("-")) {
|
|
88
|
+
const [loStr, hiStr, ...rest] = rangePart.split("-");
|
|
89
|
+
if (rest.length > 0) return null;
|
|
90
|
+
if (!/^\d+$/.test(loStr) || !/^\d+$/.test(hiStr)) return null;
|
|
91
|
+
lo = Number(loStr);
|
|
92
|
+
hi = Number(hiStr);
|
|
93
|
+
} else {
|
|
94
|
+
if (!/^\d+$/.test(rangePart)) return null;
|
|
95
|
+
lo = Number(rangePart);
|
|
96
|
+
// `a/step` (no upper bound) means a..max by step.
|
|
97
|
+
hi = stepPart !== undefined ? spec.max : lo;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate against the field range BEFORE dow-7 normalization so `7` is
|
|
101
|
+
// accepted (it's a valid Sunday alias) but `8` is rejected.
|
|
102
|
+
if (lo < spec.min || lo > spec.max || hi < spec.min || hi > spec.max) return null;
|
|
103
|
+
if (lo > hi) return null;
|
|
104
|
+
|
|
105
|
+
for (let v = lo; v <= hi; v += step) {
|
|
106
|
+
values.add(normalize(v));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return values.size > 0 ? values : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Parse a 5-field cron expression. Empty/whitespace is rejected here — callers
|
|
114
|
+
* treat `""` as "never" before calling. */
|
|
115
|
+
export function parseCron(expression: string): ParseCronResult {
|
|
116
|
+
const trimmed = expression.trim();
|
|
117
|
+
if (trimmed.length === 0) {
|
|
118
|
+
return { ok: false, error: "empty cron expression" };
|
|
119
|
+
}
|
|
120
|
+
const tokens = trimmed.split(/\s+/);
|
|
121
|
+
if (tokens.length !== 5) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: `expected 5 fields (minute hour day-of-month month day-of-week), got ${tokens.length}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const sets: Set<number>[] = [];
|
|
129
|
+
for (let i = 0; i < FIELDS.length; i++) {
|
|
130
|
+
const parsed = parseField(tokens[i], FIELDS[i]);
|
|
131
|
+
if (!parsed) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: `invalid ${FIELDS[i].name} field "${tokens[i]}" (allowed ${FIELDS[i].min}-${FIELDS[i].max})`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
sets.push(parsed);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
cron: {
|
|
143
|
+
minute: sets[0],
|
|
144
|
+
hour: sets[1],
|
|
145
|
+
dom: sets[2],
|
|
146
|
+
month: sets[3],
|
|
147
|
+
dow: sets[4],
|
|
148
|
+
domRestricted: tokens[2] !== "*",
|
|
149
|
+
dowRestricted: tokens[4] !== "*",
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** True if the expression parses; thin wrapper for config validation. */
|
|
155
|
+
export function isValidCron(expression: string): boolean {
|
|
156
|
+
return parseCron(expression).ok;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function matchesDay(cron: ParsedCron, date: Date): boolean {
|
|
160
|
+
const dom = cron.dom.has(date.getDate());
|
|
161
|
+
const dow = cron.dow.has(date.getDay());
|
|
162
|
+
if (cron.domRestricted && cron.dowRestricted) return dom || dow;
|
|
163
|
+
if (cron.domRestricted) return dom;
|
|
164
|
+
if (cron.dowRestricted) return dow;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** True if `date`'s local civil fields match the cron. */
|
|
169
|
+
export function matchesCron(cron: ParsedCron, date: Date): boolean {
|
|
170
|
+
return (
|
|
171
|
+
cron.minute.has(date.getMinutes()) &&
|
|
172
|
+
cron.hour.has(date.getHours()) &&
|
|
173
|
+
cron.month.has(date.getMonth() + 1) &&
|
|
174
|
+
matchesDay(cron, date)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Local civil-minute key `YYYY-MM-DD HH:mm` — used to dedupe the repeated wall
|
|
179
|
+
* minute on a DST fall-back so a run isn't fired twice for the same slot. */
|
|
180
|
+
function civilMinuteKey(date: Date): string {
|
|
181
|
+
const p = (n: number, w = 2) => String(n).padStart(w, "0");
|
|
182
|
+
return `${p(date.getFullYear(), 4)}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* First instant strictly after `after` whose LOCAL civil time matches `cron`.
|
|
187
|
+
* Returns null if none within the ~4-year cap (effectively-never schedules).
|
|
188
|
+
*
|
|
189
|
+
* @param excludeCivilMinute Skip any candidate sharing this `YYYY-MM-DD HH:mm`
|
|
190
|
+
* key. Pass the just-consumed run's scheduled civil minute when advancing
|
|
191
|
+
* `next_due_at` so a DST fall-back's repeated wall minute doesn't double-fire
|
|
192
|
+
* the same daily slot. (For sub-hourly every-N schedules, only the exact
|
|
193
|
+
* consumed minute is skipped; the other repeated-hour minutes still fire, which
|
|
194
|
+
* is correct — more real time elapsed.)
|
|
195
|
+
*/
|
|
196
|
+
export function nextOccurrence(
|
|
197
|
+
cron: ParsedCron,
|
|
198
|
+
after: Date,
|
|
199
|
+
excludeCivilMinute?: string,
|
|
200
|
+
maxSearchMs = MAX_SEARCH_MS,
|
|
201
|
+
): Date | null {
|
|
202
|
+
const afterMs = after.getTime();
|
|
203
|
+
// Align to the next whole minute strictly after `after` (never returns `after`).
|
|
204
|
+
let cursorMs = Math.floor(afterMs / MINUTE_MS) * MINUTE_MS + MINUTE_MS;
|
|
205
|
+
const capMs = afterMs + Math.max(0, Math.min(MAX_SEARCH_MS, maxSearchMs));
|
|
206
|
+
|
|
207
|
+
while (cursorMs <= capMs) {
|
|
208
|
+
const candidate = new Date(cursorMs);
|
|
209
|
+
if (matchesCron(cron, candidate)) {
|
|
210
|
+
if (!excludeCivilMinute || civilMinuteKey(candidate) !== excludeCivilMinute) {
|
|
211
|
+
return candidate;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
cursorMs += MINUTE_MS;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Convenience: parse + compute next-due epoch (ms) for a schedule string.
|
|
221
|
+
* Returns null for `""` / invalid / effectively-never schedules — the caller
|
|
222
|
+
* persists `next_due_at = NULL` in all of those cases.
|
|
223
|
+
*
|
|
224
|
+
* @param consumedScheduledAtMs When advancing after a run, pass the epoch of the
|
|
225
|
+
* slot just satisfied (the prior `next_due_at`); its civil minute is excluded
|
|
226
|
+
* to prevent the DST repeated-minute double-fire.
|
|
227
|
+
*/
|
|
228
|
+
export function nextDueAtMs(
|
|
229
|
+
expression: string,
|
|
230
|
+
afterMs: number,
|
|
231
|
+
consumedScheduledAtMs?: number,
|
|
232
|
+
maxSearchMs = MAX_SEARCH_MS,
|
|
233
|
+
): number | null {
|
|
234
|
+
if (expression.trim().length === 0) return null;
|
|
235
|
+
const parsed = parseCron(expression);
|
|
236
|
+
if (!parsed.ok) return null;
|
|
237
|
+
const exclude =
|
|
238
|
+
consumedScheduledAtMs !== undefined
|
|
239
|
+
? civilMinuteKey(new Date(consumedScheduledAtMs))
|
|
240
|
+
: undefined;
|
|
241
|
+
const next = nextOccurrence(parsed.cron, new Date(afterMs), exclude, maxSearchMs);
|
|
242
|
+
return next ? next.getTime() : null;
|
|
243
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Dreamer v2 task registry (pure — no DB imports, so the config schema
|
|
3
|
+
* can import the task names without pulling runtime code).
|
|
4
|
+
*
|
|
5
|
+
* v2 promotes the former post-phases (review-user-memories, key-files,
|
|
6
|
+
* evaluate-smart-notes) to first-class scheduled tasks alongside the agentic
|
|
7
|
+
* maintenance tasks, and assigns each a LEASE DOMAIN so disjoint-state tasks run
|
|
8
|
+
* concurrently while memory-mutating tasks serialize. See lease.ts + the A+B spec.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const CANONICAL_DREAM_TASKS = [
|
|
12
|
+
// map-memories runs BEFORE verify (it records the file mappings verify gates
|
|
13
|
+
// on) and shares the memory lease, so it leads the canonical order.
|
|
14
|
+
"map-memories",
|
|
15
|
+
"verify",
|
|
16
|
+
"verify-broad",
|
|
17
|
+
"curate",
|
|
18
|
+
"compress-cues",
|
|
19
|
+
"classify-memories",
|
|
20
|
+
"retrospective",
|
|
21
|
+
"maintain-docs",
|
|
22
|
+
"evaluate-smart-notes",
|
|
23
|
+
"review-user-memories",
|
|
24
|
+
"promote-primers",
|
|
25
|
+
"refresh-primers",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export type DreamTaskName = (typeof CANONICAL_DREAM_TASKS)[number];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The agentic tasks — those run as a generic dreamer agent session driven by
|
|
32
|
+
* `buildDreamTaskPrompt`. The other canonical tasks (map-memories, verify,
|
|
33
|
+
* verify-broad, classify-memories, review-user-memories, evaluate-smart-notes,
|
|
34
|
+
* primers, retrospective) have their own specialized runners and do NOT go
|
|
35
|
+
* through the prompt builder.
|
|
36
|
+
*/
|
|
37
|
+
export const AGENTIC_DREAM_TASKS = ["curate", "maintain-docs"] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tasks that read-modify-write the project `memories` table (+ epoch +
|
|
41
|
+
* supersede-delta rows). They SHARE one per-project "memory" lease so they
|
|
42
|
+
* serialize with each other — concurrent runs race semantically (stale-view
|
|
43
|
+
* merges/splits). Canonical run order when several are due in one drain.
|
|
44
|
+
*/
|
|
45
|
+
export const MEMORY_DOMAIN_TASKS: readonly DreamTaskName[] = [
|
|
46
|
+
"map-memories",
|
|
47
|
+
"verify",
|
|
48
|
+
"verify-broad",
|
|
49
|
+
"curate",
|
|
50
|
+
"compress-cues",
|
|
51
|
+
"classify-memories",
|
|
52
|
+
"retrospective",
|
|
53
|
+
"promote-primers",
|
|
54
|
+
"refresh-primers",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const MEMORY_DOMAIN_SET = new Set<DreamTaskName>(MEMORY_DOMAIN_TASKS);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lease KIND per task. `memory` + the three independent kinds are per-project;
|
|
61
|
+
* `user-memories` is GLOBAL (mutates the cross-project user-profile pool, so two
|
|
62
|
+
* different projects' dreamers must not review concurrently).
|
|
63
|
+
*/
|
|
64
|
+
export type LeaseKind = "memory" | "maintain-docs" | "evaluate-smart-notes" | "user-memories";
|
|
65
|
+
|
|
66
|
+
export function leaseKindFor(task: DreamTaskName): LeaseKind {
|
|
67
|
+
if (MEMORY_DOMAIN_SET.has(task)) return "memory";
|
|
68
|
+
switch (task) {
|
|
69
|
+
case "review-user-memories":
|
|
70
|
+
return "user-memories";
|
|
71
|
+
case "promote-primers":
|
|
72
|
+
case "refresh-primers":
|
|
73
|
+
return "memory";
|
|
74
|
+
case "maintain-docs":
|
|
75
|
+
return "maintain-docs";
|
|
76
|
+
case "evaluate-smart-notes":
|
|
77
|
+
return "evaluate-smart-notes";
|
|
78
|
+
default:
|
|
79
|
+
// Memory-domain tasks already returned above; this is unreachable.
|
|
80
|
+
return "memory";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the concrete lease key for a task in a project. The global
|
|
86
|
+
* `user-memories` lease is NOT project-scoped (one reviewer across all projects);
|
|
87
|
+
* every other domain is keyed by project so different projects never block.
|
|
88
|
+
*/
|
|
89
|
+
export function leaseKeyFor(task: DreamTaskName, projectIdentity: string): string {
|
|
90
|
+
const kind = leaseKindFor(task);
|
|
91
|
+
return kind === "user-memories" ? "user-memories" : `${kind}:${projectIdentity}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isCanonicalDreamTask(value: string): value is DreamTaskName {
|
|
95
|
+
return (CANONICAL_DREAM_TASKS as readonly string[]).includes(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stable canonical ordering used when multiple due tasks share a lease domain
|
|
100
|
+
* (preserves the suite order for the memory domain).
|
|
101
|
+
*/
|
|
102
|
+
export function compareTaskOrder(a: DreamTaskName, b: DreamTaskName): number {
|
|
103
|
+
return CANONICAL_DREAM_TASKS.indexOf(a) - CANONICAL_DREAM_TASKS.indexOf(b);
|
|
104
|
+
}
|
|
@@ -15,6 +15,7 @@ import { detectConflicts } from "./conflict-detector";
|
|
|
15
15
|
describe("detectConflicts", () => {
|
|
16
16
|
let projectDir: string;
|
|
17
17
|
let userConfigDir: string;
|
|
18
|
+
let homeDir: string;
|
|
18
19
|
let originalEnv: Record<string, string | undefined>;
|
|
19
20
|
|
|
20
21
|
beforeEach(() => {
|
|
@@ -23,6 +24,8 @@ describe("detectConflicts", () => {
|
|
|
23
24
|
mkdirSync(projectDir, { recursive: true });
|
|
24
25
|
userConfigDir = join(root, "user-config", "opencode");
|
|
25
26
|
mkdirSync(userConfigDir, { recursive: true });
|
|
27
|
+
homeDir = join(root, "home");
|
|
28
|
+
mkdirSync(homeDir, { recursive: true });
|
|
26
29
|
|
|
27
30
|
// Save and override every env var that affects config-path resolution.
|
|
28
31
|
// OPENCODE_CONFIG_DIR takes precedence over XDG_CONFIG_HOME, so we set
|
|
@@ -32,8 +35,10 @@ describe("detectConflicts", () => {
|
|
|
32
35
|
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
|
33
36
|
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
|
34
37
|
OPENCODE_DISABLE_AUTOCOMPACT: process.env.OPENCODE_DISABLE_AUTOCOMPACT,
|
|
38
|
+
HOME: process.env.HOME,
|
|
35
39
|
};
|
|
36
40
|
process.env.OPENCODE_CONFIG_DIR = userConfigDir;
|
|
41
|
+
process.env.HOME = homeDir;
|
|
37
42
|
delete process.env.XDG_CONFIG_HOME;
|
|
38
43
|
// Disable auto-compaction default during tests so we isolate plugin
|
|
39
44
|
// detection from compaction detection.
|
|
@@ -166,7 +171,7 @@ describe("detectConflicts", () => {
|
|
|
166
171
|
expect(result.conflicts.omoPreemptiveCompaction).toBe(true);
|
|
167
172
|
});
|
|
168
173
|
|
|
169
|
-
it("respects disabled_hooks in project-level OMO config", () => {
|
|
174
|
+
it("respects disabled_hooks in project-level OMO config (old format)", () => {
|
|
170
175
|
writeProjectConfig(["oh-my-opencode"]);
|
|
171
176
|
// Use project-scoped OMO config to avoid relying on user
|
|
172
177
|
// config-path resolution, which can be leaked across files
|
|
@@ -184,6 +189,130 @@ describe("detectConflicts", () => {
|
|
|
184
189
|
const result = detectConflicts(projectDir);
|
|
185
190
|
expect(result.hasConflict).toBe(false);
|
|
186
191
|
});
|
|
192
|
+
|
|
193
|
+
// --- New unified omo.jsonc (oh-my-openagent >= 4.19.0) ---
|
|
194
|
+
|
|
195
|
+
it("detects disabled_hooks in new ~/.omo/omo.jsonc (user-level)", () => {
|
|
196
|
+
writeProjectConfig(["oh-my-opencode"]);
|
|
197
|
+
const omoDir = join(homeDir, ".omo");
|
|
198
|
+
mkdirSync(omoDir, { recursive: true });
|
|
199
|
+
writeFileSync(
|
|
200
|
+
join(omoDir, "omo.jsonc"),
|
|
201
|
+
JSON.stringify({
|
|
202
|
+
"[opencode]": {
|
|
203
|
+
disabled_hooks: [
|
|
204
|
+
"preemptive-compaction",
|
|
205
|
+
"context-window-monitor",
|
|
206
|
+
"anthropic-context-window-limit-recovery",
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
const result = detectConflicts(projectDir);
|
|
212
|
+
expect(result.hasConflict).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("detects disabled_hooks in new .omo/omo.jsonc (project-level)", () => {
|
|
216
|
+
writeProjectConfig(["oh-my-opencode"]);
|
|
217
|
+
const omoDir = join(projectDir, ".omo");
|
|
218
|
+
mkdirSync(omoDir, { recursive: true });
|
|
219
|
+
writeFileSync(
|
|
220
|
+
join(omoDir, "omo.jsonc"),
|
|
221
|
+
JSON.stringify({
|
|
222
|
+
"[opencode]": {
|
|
223
|
+
disabled_hooks: [
|
|
224
|
+
"preemptive-compaction",
|
|
225
|
+
"context-window-monitor",
|
|
226
|
+
"anthropic-context-window-limit-recovery",
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
const result = detectConflicts(projectDir);
|
|
232
|
+
expect(result.hasConflict).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("detects hooks as active when new omo.jsonc has no disabled_hooks", () => {
|
|
236
|
+
writeProjectConfig(["oh-my-opencode"]);
|
|
237
|
+
const omoDir = join(homeDir, ".omo");
|
|
238
|
+
mkdirSync(omoDir, { recursive: true });
|
|
239
|
+
writeFileSync(
|
|
240
|
+
join(omoDir, "omo.jsonc"),
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
"[opencode]": {
|
|
243
|
+
// No disabled_hooks — hooks default ACTIVE
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
const result = detectConflicts(projectDir);
|
|
248
|
+
expect(result.conflicts.omoPreemptiveCompaction).toBe(true);
|
|
249
|
+
expect(result.conflicts.omoContextWindowMonitor).toBe(true);
|
|
250
|
+
expect(result.conflicts.omoAnthropicRecovery).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("reads disabled_hooks from both old and new config paths", () => {
|
|
254
|
+
writeProjectConfig(["oh-my-opencode"]);
|
|
255
|
+
// Old path: only disables preemptive-compaction
|
|
256
|
+
writeFileSync(
|
|
257
|
+
join(projectDir, "oh-my-opencode.json"),
|
|
258
|
+
JSON.stringify({
|
|
259
|
+
disabled_hooks: ["preemptive-compaction"],
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
// New path: disables the other two
|
|
263
|
+
const omoDir = join(homeDir, ".omo");
|
|
264
|
+
mkdirSync(omoDir, { recursive: true });
|
|
265
|
+
writeFileSync(
|
|
266
|
+
join(omoDir, "omo.jsonc"),
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
"[opencode]": {
|
|
269
|
+
disabled_hooks: [
|
|
270
|
+
"context-window-monitor",
|
|
271
|
+
"anthropic-context-window-limit-recovery",
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
const result = detectConflicts(projectDir);
|
|
277
|
+
// All three are disabled across both configs
|
|
278
|
+
expect(result.hasConflict).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("reads omo.json (fallback) when omo.jsonc does not exist", () => {
|
|
282
|
+
writeProjectConfig(["oh-my-opencode"]);
|
|
283
|
+
const omoDir = join(homeDir, ".omo");
|
|
284
|
+
mkdirSync(omoDir, { recursive: true });
|
|
285
|
+
writeFileSync(
|
|
286
|
+
join(omoDir, "omo.json"),
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
"[opencode]": {
|
|
289
|
+
disabled_hooks: [
|
|
290
|
+
"preemptive-compaction",
|
|
291
|
+
"context-window-monitor",
|
|
292
|
+
"anthropic-context-window-limit-recovery",
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
const result = detectConflicts(projectDir);
|
|
298
|
+
expect(result.hasConflict).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("ignores new omo.jsonc when OMO is not installed", () => {
|
|
302
|
+
writeProjectConfig([]);
|
|
303
|
+
const omoDir = join(homeDir, ".omo");
|
|
304
|
+
mkdirSync(omoDir, { recursive: true });
|
|
305
|
+
writeFileSync(
|
|
306
|
+
join(omoDir, "omo.jsonc"),
|
|
307
|
+
JSON.stringify({
|
|
308
|
+
"[opencode]": {
|
|
309
|
+
disabled_hooks: ["preemptive-compaction"],
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
const result = detectConflicts(projectDir);
|
|
314
|
+
expect(result.hasConflict).toBe(false);
|
|
315
|
+
});
|
|
187
316
|
});
|
|
188
317
|
|
|
189
318
|
// --- Combined / control cases ---
|
|
@@ -242,4 +371,125 @@ describe("detectConflicts", () => {
|
|
|
242
371
|
expect(result.hasConflict).toBe(false);
|
|
243
372
|
});
|
|
244
373
|
});
|
|
374
|
+
|
|
375
|
+
// --- Compaction-off mode matrix (issue #266 S2) ---
|
|
376
|
+
// The detector must NOT flag OpenCode compaction.auto=true / prune=true as
|
|
377
|
+
// a plugin-disabling conflict when MC compaction is OFF (compaction-off
|
|
378
|
+
// mode), or compaction-off users get a DISABLED plugin — the exact inverse
|
|
379
|
+
// of intent. With MC compaction ON, today's conflict behavior is unchanged.
|
|
380
|
+
//
|
|
381
|
+
// The 2x2 matrix: MC mode (on/off) x native compaction.auto (true/false).
|
|
382
|
+
// Each case asserts BOTH the conflict verdict AND the plugin-enabled
|
|
383
|
+
// outcome (the boot path disables the plugin when hasConflict).
|
|
384
|
+
describe("compaction-off mode matrix (issue #266)", () => {
|
|
385
|
+
// The suite beforeEach sets OPENCODE_DISABLE_AUTOCOMPACT=1 to isolate
|
|
386
|
+
// plugin detection from compaction detection. These matrix tests
|
|
387
|
+
// exercise compaction detection directly, so they clear that env var
|
|
388
|
+
// and write an explicit compaction block to control the native state.
|
|
389
|
+
function writeCompactionConfig(auto: boolean, prune = false): void {
|
|
390
|
+
const prev = process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
391
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
392
|
+
writeFileSync(
|
|
393
|
+
join(projectDir, "opencode.json"),
|
|
394
|
+
JSON.stringify({ compaction: { auto, prune } }),
|
|
395
|
+
);
|
|
396
|
+
if (prev !== undefined) process.env.OPENCODE_DISABLE_AUTOCOMPACT = prev;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function detectWithMode(compactionEnabled: boolean) {
|
|
400
|
+
const prev = process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
401
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
402
|
+
try {
|
|
403
|
+
return detectConflicts(projectDir, { compactionEnabled });
|
|
404
|
+
} finally {
|
|
405
|
+
if (prev !== undefined) process.env.OPENCODE_DISABLE_AUTOCOMPACT = prev;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
it("MC ON + auto=true → conflict fires, plugin would be disabled", () => {
|
|
410
|
+
writeCompactionConfig(true);
|
|
411
|
+
const result = detectWithMode(true);
|
|
412
|
+
expect(result.hasConflict).toBe(true);
|
|
413
|
+
expect(result.conflicts.compactionAuto).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("MC ON + auto=false → no compaction conflict, plugin stays enabled", () => {
|
|
417
|
+
writeCompactionConfig(false);
|
|
418
|
+
const result = detectWithMode(true);
|
|
419
|
+
expect(result.conflicts.compactionAuto).toBe(false);
|
|
420
|
+
expect(result.hasConflict).toBe(false);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("MC OFF + auto=true → NO conflict, plugin stays enabled (native compaction active)", () => {
|
|
424
|
+
writeCompactionConfig(true);
|
|
425
|
+
const result = detectWithMode(false);
|
|
426
|
+
expect(result.conflicts.compactionAuto).toBe(false);
|
|
427
|
+
expect(result.conflicts.compactionPrune).toBe(false);
|
|
428
|
+
expect(result.hasConflict).toBe(false);
|
|
429
|
+
// Native compaction state is still reported honestly.
|
|
430
|
+
expect(result.nativeCompaction.auto).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("MC OFF + auto=false → NO conflict, no-manager configuration reported honestly", () => {
|
|
434
|
+
writeCompactionConfig(false);
|
|
435
|
+
const result = detectWithMode(false);
|
|
436
|
+
expect(result.conflicts.compactionAuto).toBe(false);
|
|
437
|
+
expect(result.hasConflict).toBe(false);
|
|
438
|
+
// No-manager: neither MC nor native compaction owns the window.
|
|
439
|
+
expect(result.nativeCompaction.auto).toBe(false);
|
|
440
|
+
expect(result.nativeCompaction.prune).toBe(false);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Mutation direction: force mode-on in the detector with auto=true →
|
|
444
|
+
// conflict fires. This proves the off-gate isn't just always-pass —
|
|
445
|
+
// the same native config that was NOT a conflict in the off case
|
|
446
|
+
// becomes a conflict when the mode is forced on.
|
|
447
|
+
it("mutation direction: same auto=true config DOES conflict when mode forced on", () => {
|
|
448
|
+
writeCompactionConfig(true);
|
|
449
|
+
const offResult = detectWithMode(false);
|
|
450
|
+
const onResult = detectWithMode(true);
|
|
451
|
+
expect(offResult.hasConflict).toBe(false);
|
|
452
|
+
expect(onResult.hasConflict).toBe(true);
|
|
453
|
+
expect(onResult.conflicts.compactionAuto).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// prune=true follows the same gate as auto=true.
|
|
457
|
+
it("MC OFF + prune=true → NO conflict (prune is not a conflict in compaction-off mode)", () => {
|
|
458
|
+
writeCompactionConfig(false, true);
|
|
459
|
+
const result = detectWithMode(false);
|
|
460
|
+
expect(result.conflicts.compactionPrune).toBe(false);
|
|
461
|
+
expect(result.hasConflict).toBe(false);
|
|
462
|
+
expect(result.nativeCompaction.prune).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// DCP and OMO conflicts keep their existing policy in BOTH modes.
|
|
466
|
+
it("MC OFF + DCP plugin → DCP conflict still fires (compaction-off does not broaden compatibility)", () => {
|
|
467
|
+
writeProjectConfig(["@tarquinen/opencode-dcp"]);
|
|
468
|
+
const result = detectWithMode(false);
|
|
469
|
+
expect(result.conflicts.dcpPlugin).toBe(true);
|
|
470
|
+
expect(result.hasConflict).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("MC OFF + OMO hooks → OMO conflicts still fire in both modes", () => {
|
|
474
|
+
writeProjectConfig(["oh-my-opencode"]);
|
|
475
|
+
const result = detectWithMode(false);
|
|
476
|
+
expect(result.conflicts.omoPreemptiveCompaction).toBe(true);
|
|
477
|
+
expect(result.hasConflict).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Default (no options) preserves today's mode-on behavior — a call
|
|
481
|
+
// site that cannot supply the resolved mode fails toward mode-on.
|
|
482
|
+
it("default (no options) treats compaction.auto=true as a conflict (fail toward mode-on)", () => {
|
|
483
|
+
writeCompactionConfig(true);
|
|
484
|
+
const prev = process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
485
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
486
|
+
try {
|
|
487
|
+
const result = detectConflicts(projectDir);
|
|
488
|
+
expect(result.conflicts.compactionAuto).toBe(true);
|
|
489
|
+
expect(result.hasConflict).toBe(true);
|
|
490
|
+
} finally {
|
|
491
|
+
if (prev !== undefined) process.env.OPENCODE_DISABLE_AUTOCOMPACT = prev;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
245
495
|
});
|