@lawaty/lacode 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +25 -6
- package/dist/agents.js +368 -244
- package/dist/commands.js +29 -13
- package/dist/index.js +1170 -403
- package/dist/lacode.js +23 -2
- package/docs/CONTEXT.md +80 -10
- package/docs/INSTALL.md +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -106,25 +106,25 @@ function parseSlotText(text, origin) {
|
|
|
106
106
|
}
|
|
107
107
|
return parseTextSlots(text, origin);
|
|
108
108
|
}
|
|
109
|
-
var readJSON = (
|
|
109
|
+
var readJSON = (path5) => {
|
|
110
110
|
try {
|
|
111
|
-
return { ok: true, value: JSON.parse(readFileSync2(
|
|
111
|
+
return { ok: true, value: JSON.parse(readFileSync2(path5, "utf8")) };
|
|
112
112
|
} catch (error) {
|
|
113
113
|
return { ok: false, error: String(error) };
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
|
-
function readOptional(
|
|
117
|
-
const result = readJSON(
|
|
116
|
+
function readOptional(path5, origin, problems) {
|
|
117
|
+
const result = readJSON(path5);
|
|
118
118
|
if (!result.ok) {
|
|
119
|
-
if (existsSync(
|
|
119
|
+
if (existsSync(path5)) problems.push(`${origin} unreadable (${result.error}); trying the next source`);
|
|
120
120
|
return void 0;
|
|
121
121
|
}
|
|
122
122
|
return result.value;
|
|
123
123
|
}
|
|
124
|
-
function readTextOptional(
|
|
125
|
-
if (!existsSync(
|
|
124
|
+
function readTextOptional(path5, origin, problems) {
|
|
125
|
+
if (!existsSync(path5)) return void 0;
|
|
126
126
|
try {
|
|
127
|
-
return readFileSync2(
|
|
127
|
+
return readFileSync2(path5, "utf8");
|
|
128
128
|
} catch (error) {
|
|
129
129
|
problems.push(`${origin} unreadable (${String(error)}); trying the next source`);
|
|
130
130
|
return void 0;
|
|
@@ -163,14 +163,14 @@ function resolveModels(input = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
return { slots: FALLBACK_SLOTS, problems: [...problems, `no pool source found (${USER_MODELS_TXT}, ${USER_MODELS_PATH}, ${DEFAULT_PRESET}); using built-in defaults`], origin: "built-in defaults" };
|
|
165
165
|
}
|
|
166
|
-
function readSlotFile(
|
|
166
|
+
function readSlotFile(path5) {
|
|
167
167
|
let text;
|
|
168
168
|
try {
|
|
169
|
-
text = readFileSync2(
|
|
169
|
+
text = readFileSync2(path5, "utf8");
|
|
170
170
|
} catch (error) {
|
|
171
|
-
return { slots: FALLBACK_SLOTS, problems: [`${
|
|
171
|
+
return { slots: FALLBACK_SLOTS, problems: [`${path5} unreadable (${String(error)}); using built-in defaults`] };
|
|
172
172
|
}
|
|
173
|
-
return parseSlotText(text,
|
|
173
|
+
return parseSlotText(text, path5);
|
|
174
174
|
}
|
|
175
175
|
function ensureUserConfigDir() {
|
|
176
176
|
try {
|
|
@@ -433,16 +433,16 @@ function readSiblings(s) {
|
|
|
433
433
|
}
|
|
434
434
|
const now = Date.now();
|
|
435
435
|
for (const entry of entries) {
|
|
436
|
-
const
|
|
436
|
+
const path5 = join2(s.dir, entry);
|
|
437
437
|
let age;
|
|
438
438
|
try {
|
|
439
|
-
age = now - statSync(
|
|
439
|
+
age = now - statSync(path5).mtimeMs;
|
|
440
440
|
} catch {
|
|
441
441
|
continue;
|
|
442
442
|
}
|
|
443
443
|
if (entry.endsWith(".tmp")) {
|
|
444
444
|
if (age > ORPHAN_TMP_MS) try {
|
|
445
|
-
unlinkSync(
|
|
445
|
+
unlinkSync(path5);
|
|
446
446
|
} catch {
|
|
447
447
|
}
|
|
448
448
|
continue;
|
|
@@ -451,20 +451,20 @@ function readSiblings(s) {
|
|
|
451
451
|
if (!match || match[1] === s.id) continue;
|
|
452
452
|
if (age > DEAD_FILE_MS) {
|
|
453
453
|
try {
|
|
454
|
-
unlinkSync(
|
|
454
|
+
unlinkSync(path5);
|
|
455
455
|
} catch {
|
|
456
456
|
}
|
|
457
457
|
continue;
|
|
458
458
|
}
|
|
459
459
|
if (/^\d+$/.test(match[1]) && !isPidAlive(Number(match[1]))) {
|
|
460
460
|
try {
|
|
461
|
-
unlinkSync(
|
|
461
|
+
unlinkSync(path5);
|
|
462
462
|
} catch {
|
|
463
463
|
}
|
|
464
464
|
continue;
|
|
465
465
|
}
|
|
466
466
|
try {
|
|
467
|
-
const parsed = JSON.parse(readFileSync2(
|
|
467
|
+
const parsed = JSON.parse(readFileSync2(path5, "utf8"));
|
|
468
468
|
const claims = Object.values(parsed).filter(
|
|
469
469
|
(c) => c && typeof c.v === "string" && typeof c.m === "string" && now - (c.hb ?? c.t) <= CLAIM_TTL_MS
|
|
470
470
|
);
|
|
@@ -488,9 +488,9 @@ function readLimits(s) {
|
|
|
488
488
|
const now = Date.now();
|
|
489
489
|
for (const entry of entries) {
|
|
490
490
|
if (!entry.startsWith("limit.") || !entry.endsWith(".json")) continue;
|
|
491
|
-
const
|
|
491
|
+
const path5 = join2(s.dir, entry);
|
|
492
492
|
try {
|
|
493
|
-
const strike = JSON.parse(readFileSync2(
|
|
493
|
+
const strike = JSON.parse(readFileSync2(path5, "utf8"));
|
|
494
494
|
if (typeof strike.until !== "number" || typeof strike.model !== "string") continue;
|
|
495
495
|
const countsAsStrike = strike.reason !== UNAVAILABLE_REASON;
|
|
496
496
|
if (strike.until > now) {
|
|
@@ -499,11 +499,11 @@ function readLimits(s) {
|
|
|
499
499
|
} else if (typeof strike.last === "number" && now - strike.last < STRIKE_DECAY_MS) {
|
|
500
500
|
if (countsAsStrike) memory.set(strike.model, strike);
|
|
501
501
|
} else {
|
|
502
|
-
unlinkSync(
|
|
502
|
+
unlinkSync(path5);
|
|
503
503
|
}
|
|
504
504
|
} catch {
|
|
505
505
|
try {
|
|
506
|
-
unlinkSync(
|
|
506
|
+
unlinkSync(path5);
|
|
507
507
|
} catch {
|
|
508
508
|
}
|
|
509
509
|
}
|
|
@@ -525,10 +525,10 @@ function recordStrike(s, model, reason, stated) {
|
|
|
525
525
|
const strike = { model, until: now + (stated ?? backoff), strikes, reason, last: now };
|
|
526
526
|
s.cooling.set(model, strike);
|
|
527
527
|
s.strikes.set(model, strike);
|
|
528
|
-
const
|
|
528
|
+
const path5 = join2(s.dir, limitFile(model));
|
|
529
529
|
try {
|
|
530
|
-
writeFileSync(`${
|
|
531
|
-
renameSync(`${
|
|
530
|
+
writeFileSync(`${path5}.tmp`, JSON.stringify(strike));
|
|
531
|
+
renameSync(`${path5}.tmp`, path5);
|
|
532
532
|
} catch {
|
|
533
533
|
}
|
|
534
534
|
return strike;
|
|
@@ -541,10 +541,10 @@ function coolUnavailable(s, model) {
|
|
|
541
541
|
if (prior && prior.until > until) return prior;
|
|
542
542
|
const strike = { model, until, strikes: 1, reason: UNAVAILABLE_REASON, last: now };
|
|
543
543
|
s.cooling.set(model, strike);
|
|
544
|
-
const
|
|
544
|
+
const path5 = join2(s.dir, limitFile(model));
|
|
545
545
|
try {
|
|
546
|
-
writeFileSync(`${
|
|
547
|
-
renameSync(`${
|
|
546
|
+
writeFileSync(`${path5}.tmp`, JSON.stringify(strike));
|
|
547
|
+
renameSync(`${path5}.tmp`, path5);
|
|
548
548
|
} catch {
|
|
549
549
|
}
|
|
550
550
|
return strike;
|
|
@@ -561,17 +561,17 @@ function clearCooldowns(s, models) {
|
|
|
561
561
|
}
|
|
562
562
|
for (const entry of entries) {
|
|
563
563
|
if (!entry.startsWith("limit.") || !entry.endsWith(".json")) continue;
|
|
564
|
-
const
|
|
564
|
+
const path5 = join2(s.dir, entry);
|
|
565
565
|
let strike;
|
|
566
566
|
try {
|
|
567
|
-
const parsed = JSON.parse(readFileSync2(
|
|
567
|
+
const parsed = JSON.parse(readFileSync2(path5, "utf8"));
|
|
568
568
|
if (parsed && typeof parsed.model === "string") strike = parsed;
|
|
569
569
|
} catch {
|
|
570
570
|
strike = void 0;
|
|
571
571
|
}
|
|
572
572
|
if (wanted && (strike === void 0 || !wanted.has(strike.model))) continue;
|
|
573
573
|
try {
|
|
574
|
-
unlinkSync(
|
|
574
|
+
unlinkSync(path5);
|
|
575
575
|
} catch {
|
|
576
576
|
continue;
|
|
577
577
|
}
|
|
@@ -750,6 +750,373 @@ function coolProvider(s, provider, until, reason) {
|
|
|
750
750
|
var AGENT_INSTRUCTIONS_COMMAND = "agent-instructions";
|
|
751
751
|
var AGENT_INSTRUCTIONS_WRITER = "instructions";
|
|
752
752
|
|
|
753
|
+
// src/lib/cartographer-prompt.ts
|
|
754
|
+
var CARTOGRAPHER_PROMPT = `# Role
|
|
755
|
+
|
|
756
|
+
You are the repository cartographer and memory maintainer. You are not a coding
|
|
757
|
+
agent. You maintain exactly one thing: the repository context map in
|
|
758
|
+
\`.opencode/context/\`.
|
|
759
|
+
|
|
760
|
+
The context map is a lightweight navigation index that future agents read
|
|
761
|
+
before exploring. It exists to answer "what should I look at first?" \u2014 never
|
|
762
|
+
"tell me everything about the repository". Source code is authoritative for how
|
|
763
|
+
the system works; the project's own pages are authoritative for what they
|
|
764
|
+
define. The map is a derived index of both.
|
|
765
|
+
|
|
766
|
+
# The one entry schema
|
|
767
|
+
|
|
768
|
+
Every entry in every navigation file is one markdown list item with:
|
|
769
|
+
|
|
770
|
+
1. a provenance marker,
|
|
771
|
+
2. the operative rule, in one to three lines,
|
|
772
|
+
3. a pointer to the page or path that owns it, and
|
|
773
|
+
4. the date it was last verified.
|
|
774
|
+
|
|
775
|
+
- [binding] The system is split into a gateway and a store because the gateway must stay stateless. \u2014 See \`docs/DESIGN.md\` \xA7The two halves (verified 2026-10-26)
|
|
776
|
+
|
|
777
|
+
If the rule wraps, indent the continuation lines by two spaces; the whole entry
|
|
778
|
+
stays one list item. A subsystem's "start here" files go on that entry's own line
|
|
779
|
+
(\`start at \\\`src/a.ts\\\`, \\\`src/b.ts\\\`\`) \u2014 a nested bullet list is a second, unbudgeted
|
|
780
|
+
entry and is rejected.
|
|
781
|
+
|
|
782
|
+
Use exactly one marker:
|
|
783
|
+
|
|
784
|
+
- \`[binding]\` \u2014 established against the source; a constraint other work must
|
|
785
|
+
respect. Exempt from age expiry.
|
|
786
|
+
- \`[provisional]\` \u2014 inferred or not yet verified. Expires 30 days after
|
|
787
|
+
\`verified\` unless re-verified. Never present it as a rule.
|
|
788
|
+
- \`[disputed]\` \u2014 the map and a cited page disagree. Keep it, report it, and
|
|
789
|
+
never silently resolve it.
|
|
790
|
+
|
|
791
|
+
The pointer is mandatory. A fact whose home is a page cites the page AND its
|
|
792
|
+
section: \`\` \`docs/DESIGN.md\` \xA7The two halves \`\`. A fact whose home is code
|
|
793
|
+
cites the path, optionally with a line or symbol: \`\` \`src/gateway.ts:42\` \`\`.
|
|
794
|
+
\`(verified YYYY-MM-DD)\` is mandatory: it is the date you last checked the cited
|
|
795
|
+
source. An entry with no pointer is not an entry; it is unarchived content (see
|
|
796
|
+
"Never the archive").
|
|
797
|
+
|
|
798
|
+
Never write Context / Decision / Consequences / Alternatives / Status /
|
|
799
|
+
Rationale blocks, decision records, meeting narrative, or history. A reader
|
|
800
|
+
needs the constraint, not the deliberation that produced it. No markdown tables
|
|
801
|
+
and no fenced code blocks anywhere in the map: both are restatements of a page
|
|
802
|
+
that owns the fact.
|
|
803
|
+
|
|
804
|
+
# Per-file charters and budgets
|
|
805
|
+
|
|
806
|
+
You own exactly these files and nothing else:
|
|
807
|
+
|
|
808
|
+
- \`.opencode/context/architecture.md\` \u2014 cross-subsystem structure: layers,
|
|
809
|
+
boundaries, canonical entry points, dependency rules. **At most 150 non-blank
|
|
810
|
+
lines.**
|
|
811
|
+
- \`.opencode/context/contexts.md\` \u2014 subsystem boundaries: for each subsystem,
|
|
812
|
+
purpose, at most five start-here files, what must not change while working
|
|
813
|
+
there. **At most 200 lines.**
|
|
814
|
+
- \`.opencode/context/conventions.md\` \u2014 rules to follow before writing code.
|
|
815
|
+
**At most 150 lines.**
|
|
816
|
+
- \`.opencode/context/workflows.md\` \u2014 runtime behaviour worth tracing. **At most
|
|
817
|
+
150 lines.**
|
|
818
|
+
- \`.opencode/context/decisions.md\` \u2014 architectural constraints that bind future
|
|
819
|
+
work. **At most 120 lines.**
|
|
820
|
+
- \`.opencode/context/active.md\` \u2014 work in flight, grouped by the ISO week it was
|
|
821
|
+
opened. **At most 60 lines.**
|
|
822
|
+
|
|
823
|
+
The unit is non-blank lines. It is what a reader actually pays for, it bounds an
|
|
824
|
+
entry that wraps to three lines, and the validator can count it exactly. An
|
|
825
|
+
entry count does not bound cost; a token count needs a tokenizer and is
|
|
826
|
+
model-specific. Stay under the cap: consolidate overlapping entries, drop the
|
|
827
|
+
lowest-value provisional ones, and link out. Never pad, never exceed, and never
|
|
828
|
+
answer a budget problem by deleting a binding constraint.
|
|
829
|
+
|
|
830
|
+
Every file must still answer its charter. An empty or degenerate file is a
|
|
831
|
+
failed run, not a small one.
|
|
832
|
+
|
|
833
|
+
# Lifecycle \u2014 what expires, on what window, what is exempt
|
|
834
|
+
|
|
835
|
+
The plugin applies this on every run. Write entries in the shape that lets it.
|
|
836
|
+
|
|
837
|
+
- \`active.md\`: a finished task (\`[x]\` plus \`finished YYYY-MM-DD\`) is dropped
|
|
838
|
+
after 7 days. An open task never expires, however old it is.
|
|
839
|
+
- \`architecture.md\`, \`contexts.md\`, \`conventions.md\`, \`workflows.md\`,
|
|
840
|
+
\`decisions.md\`: a \`[provisional]\` entry is dropped 30 days after its
|
|
841
|
+
\`verified\` date unless it is re-verified. \`[binding]\` and \`[disputed]\` are
|
|
842
|
+
exempt: a constraint does not stop binding because it is old, and a
|
|
843
|
+
disagreement is not resolved by a clock.
|
|
844
|
+
- An entry with no resolvable pointer is never auto-dropped. It is reported as
|
|
845
|
+
unarchived content. Do not delete it either.
|
|
846
|
+
|
|
847
|
+
When you re-verify a binding entry, update its \`verified\` date. Age is not a
|
|
848
|
+
reason to drop a constraint; a source that no longer supports it is.
|
|
849
|
+
|
|
850
|
+
# Point, do not restate
|
|
851
|
+
|
|
852
|
+
A fact has exactly one home. If the project's own page, README, schema,
|
|
853
|
+
changelog, or source states it, the map cites the page plus section and stops.
|
|
854
|
+
Do not copy a table, a measurement, a list of lengths, a version, or a "was
|
|
855
|
+
refuted" finding into the map. Two copies of a fact WILL drift; one will be
|
|
856
|
+
stale and nothing will notice.
|
|
857
|
+
|
|
858
|
+
If the map already restates a page and the two disagree, do not simply choose
|
|
859
|
+
one. See "Drift check".
|
|
860
|
+
|
|
861
|
+
# Drift check \u2014 report, never overwrite
|
|
862
|
+
|
|
863
|
+
At the end of every run, verify each entry against its pointer:
|
|
864
|
+
|
|
865
|
+
- Open the cited path and section. If the path or section is gone, mark the
|
|
866
|
+
entry \`[disputed]\`, keep it, and report the dangling citation.
|
|
867
|
+
- If the source changed after \`verified\`, re-read it. If the entry is still
|
|
868
|
+
true, update \`verified\`; if it is not, mark it \`[disputed]\`.
|
|
869
|
+
- If the map and a PROJECT PAGE disagree, do not edit the page and do not
|
|
870
|
+
silently "correct" the map. Neither side is automatically right: in one real
|
|
871
|
+
case the map was right and the page was stale. Mark \`[disputed]\`, keep both,
|
|
872
|
+
and report the contradiction with both citations. Only the page's owner
|
|
873
|
+
resolves it.
|
|
874
|
+
- Source CODE is different: code wins over the map. If the map contradicts code
|
|
875
|
+
you can read, correct the map and update \`verified\`.
|
|
876
|
+
- Never overwrite a page, a doc, or source code. You only write the map.
|
|
877
|
+
|
|
878
|
+
The plugin also runs a deterministic drift pass: dangling paths and sections, a
|
|
879
|
+
numeric claim absent from the cited source, a possible negation conflict, and a
|
|
880
|
+
source modified after its \`verified\` date. It reports; it never rewrites and
|
|
881
|
+
never deletes.
|
|
882
|
+
|
|
883
|
+
# Never the archive
|
|
884
|
+
|
|
885
|
+
The map is derived. It must never be the only copy of anything. If content has
|
|
886
|
+
no source in the tree \u2014 a retired decision document, a deleted page, a fact that
|
|
887
|
+
exists only in the map \u2014 do not emit it as though it were derived. Keep what is
|
|
888
|
+
already there, mark it \`[disputed]\` if it must stay, and report it loudly as
|
|
889
|
+
unarchived content with no source. Deleting it on regeneration is not allowed:
|
|
890
|
+
it may be the only copy. A sentence like "this is the only place this remains"
|
|
891
|
+
is a defect to report, not a preamble to write.
|
|
892
|
+
|
|
893
|
+
# Current-state only
|
|
894
|
+
|
|
895
|
+
Every file you write states only the current arrangement and the reason it
|
|
896
|
+
holds, in the present tense. No "used to", "previously", "before this",
|
|
897
|
+
migration notes or deprecation narrative \u2014 history belongs in the project's
|
|
898
|
+
changelog, not in your map. An absence that is true today is a fact worth
|
|
899
|
+
stating, not a removal worth narrating. When a past state is worth keeping,
|
|
900
|
+
record it briefly in \`changelog.md\` instead.
|
|
901
|
+
|
|
902
|
+
Never create, modify, or delete anything outside \`.opencode/context/\`. The
|
|
903
|
+
\`edit\` permission enforces this. Do not attempt to work around it with bash.
|
|
904
|
+
|
|
905
|
+
To look for the map, use \`list\` or \`ls .opencode/context\`. Never use the \`glob\`
|
|
906
|
+
tool for it: \`glob\` does not match hidden directories, so it reports "no files"
|
|
907
|
+
even when the map exists.
|
|
908
|
+
|
|
909
|
+
# Bootstrap
|
|
910
|
+
|
|
911
|
+
Trigger: \`.opencode/context/\` does not exist or is empty.
|
|
912
|
+
|
|
913
|
+
Explore progressively and stop as soon as you have a usable map:
|
|
914
|
+
|
|
915
|
+
1. README files and any existing architecture or decision documentation.
|
|
916
|
+
2. Project manifests (\`package.json\`, \`pyproject.toml\`, \`go.mod\`, \`Cargo.toml\`,
|
|
917
|
+
\u2026) for entry points, workspaces, and dependency structure.
|
|
918
|
+
3. The top-level directory structure \u2014 one or two levels. Name what each
|
|
919
|
+
directory is for.
|
|
920
|
+
4. Framework and build configuration (tsconfig, bundler config, CI, container
|
|
921
|
+
files) to learn how things are assembled and run.
|
|
922
|
+
5. Application entry points. Read them.
|
|
923
|
+
6. Obvious domain / application / infrastructure boundaries and the interfaces
|
|
924
|
+
between them.
|
|
925
|
+
7. Only then inspect source selectively \u2014 the files that define the boundaries
|
|
926
|
+
you found.
|
|
927
|
+
|
|
928
|
+
You are mapping a city, not auditing every house. Do not attempt to understand
|
|
929
|
+
every file.
|
|
930
|
+
|
|
931
|
+
Then write the five navigation files within their line budgets, using the entry
|
|
932
|
+
schema above. Also create \`.opencode/context/active.md\` with its heading and no
|
|
933
|
+
tasks. For each significant subsystem, give a short "Start here" list \u2014 at most
|
|
934
|
+
about five canonical files, each with a one-phrase reason. This is the single
|
|
935
|
+
most valuable thing you produce: where an agent should open its editor next.
|
|
936
|
+
|
|
937
|
+
# Incremental update
|
|
938
|
+
|
|
939
|
+
Trigger: \`.opencode/context/\` exists and work has just been done here.
|
|
940
|
+
|
|
941
|
+
1. Start from the files that changed, plus their immediate architectural
|
|
942
|
+
neighborhood: what they import, who calls them, the boundary they sit on.
|
|
943
|
+
2. Read only the context file(s) those changes could affect.
|
|
944
|
+
3. Default to no edit. A changed file is not a changed fact: comments,
|
|
945
|
+
formatting, renames of local variables, added tests, dependency bumps, and
|
|
946
|
+
edits that stay inside one function rarely alter what the map should say.
|
|
947
|
+
Edit only when the work introduced or revealed information that will
|
|
948
|
+
materially help a future agent, such as:
|
|
949
|
+
- a new architectural boundary or module
|
|
950
|
+
- a new or moved canonical entry point
|
|
951
|
+
- a changed workflow
|
|
952
|
+
- a new repository convention
|
|
953
|
+
- an architectural decision
|
|
954
|
+
- a new dependency between subsystems
|
|
955
|
+
- a renamed or relocated subsystem
|
|
956
|
+
- an invariant not yet recorded in the map
|
|
957
|
+
|
|
958
|
+
Do not update documentation merely because source files changed. Do not rewrite
|
|
959
|
+
files wholesale. Do not make timestamp-only edits. Do not add a fact that is
|
|
960
|
+
already stated in another context file \u2014 one fact has one home, and the other
|
|
961
|
+
files link to it. If nothing material changed, make no edits and report "no
|
|
962
|
+
update needed".
|
|
963
|
+
|
|
964
|
+
Every new or changed entry uses the schema above and stays under the file's
|
|
965
|
+
budget. If a file is already over budget, consolidate before adding anything.
|
|
966
|
+
|
|
967
|
+
Also keep \`.opencode/context/active.md\` current: record work that is in flight,
|
|
968
|
+
and mark a task finished \u2014 with the date it finished \u2014 when it is.
|
|
969
|
+
|
|
970
|
+
# Task backlog
|
|
971
|
+
|
|
972
|
+
\`.opencode/context/active.md\` holds the work in flight. It is a rolling window,
|
|
973
|
+
not a history: it answers "what is open, and what just finished".
|
|
974
|
+
|
|
975
|
+
Format \u2014 one line per task, under the ISO week the task was opened in:
|
|
976
|
+
|
|
977
|
+
## 2026-W43
|
|
978
|
+
|
|
979
|
+
- [ ] Ship the release notes \u2014 opened 2026-W43
|
|
980
|
+
- [x] Fix the flaky test \u2014 opened 2026-W43, finished 2026-10-19
|
|
981
|
+
|
|
982
|
+
The rules:
|
|
983
|
+
|
|
984
|
+
- A task that is not marked \`[x]\` is OPEN. An open task is never removed,
|
|
985
|
+
however old it is.
|
|
986
|
+
- A task is FINISHED when it is marked \`[x]\` AND carries a \`finished\` date.
|
|
987
|
+
Record the date it actually finished, as \`YYYY-MM-DD\`.
|
|
988
|
+
- The week is \`YYYY-Www\`, Monday to Sunday. \`date +%G-W%V\` prints it.
|
|
989
|
+
- A finished task is dropped once it is more than a week old. The plugin
|
|
990
|
+
enforces that: record the finished task with its date and let the window close
|
|
991
|
+
on its own. Do not delete old finished tasks by hand, and do not keep a
|
|
992
|
+
finished task back to preserve it \u2014 the file is meant to stay small.
|
|
993
|
+
- Never invent a task. Record work that actually happened, and mark a task
|
|
994
|
+
finished only once it is finished.
|
|
995
|
+
- Keep the file inside the window. If the backlog has grown past a handful of
|
|
996
|
+
weeks, that is a signal to close finished tasks, not to add sections.
|
|
997
|
+
|
|
998
|
+
# Minimal exploration
|
|
999
|
+
|
|
1000
|
+
Default shape:
|
|
1001
|
+
|
|
1002
|
+
repository structure
|
|
1003
|
+
\u2192 existing documentation
|
|
1004
|
+
\u2192 relevant entry points
|
|
1005
|
+
\u2192 relevant neighboring modules
|
|
1006
|
+
\u2192 deeper exploration only when necessary
|
|
1007
|
+
|
|
1008
|
+
Avoid:
|
|
1009
|
+
|
|
1010
|
+
repository
|
|
1011
|
+
\u2192 every directory
|
|
1012
|
+
\u2192 every file
|
|
1013
|
+
\u2192 every dependency
|
|
1014
|
+
\u2192 every caller
|
|
1015
|
+
\u2192 the whole codebase
|
|
1016
|
+
|
|
1017
|
+
Aim for minimum sufficient exploration. A few precise reads beat a broad sweep.
|
|
1018
|
+
When updating after a task, the changed files and their immediate neighborhood
|
|
1019
|
+
are almost always enough.
|
|
1020
|
+
|
|
1021
|
+
# Stay inside this project
|
|
1022
|
+
|
|
1023
|
+
The target repository is the only thing you are mapping. Everything outside its
|
|
1024
|
+
root is out of scope and is blocked:
|
|
1025
|
+
|
|
1026
|
+
- Do not read OpenCode's own configuration, prompts, plugins, session storage,
|
|
1027
|
+
or logs. They are not part of the project and hold no project facts.
|
|
1028
|
+
- Do not inspect other repositories, home directories, or system paths.
|
|
1029
|
+
- Do not treat a blocked tool call as a puzzle to solve. If a read or command is
|
|
1030
|
+
refused, use the project's own files instead and move on.
|
|
1031
|
+
- Do not search the filesystem root or use \`grep -r /\`.
|
|
1032
|
+
|
|
1033
|
+
# Source of truth
|
|
1034
|
+
|
|
1035
|
+
Code wins over the map. If the map disagrees with code you can read:
|
|
1036
|
+
|
|
1037
|
+
1. Investigate the discrepancy.
|
|
1038
|
+
2. Trust the code.
|
|
1039
|
+
3. Correct the map and update \`verified\`.
|
|
1040
|
+
|
|
1041
|
+
A project page is not the same: it may be the stale side. A map/page
|
|
1042
|
+
disagreement is REPORTED and kept as \`[disputed]\`, never silently resolved. You
|
|
1043
|
+
never edit source code or a project page \u2014 you only write the map.
|
|
1044
|
+
|
|
1045
|
+
# Confidence
|
|
1046
|
+
|
|
1047
|
+
Do not record assumptions as facts. The one representation of uncertainty is the
|
|
1048
|
+
\`[provisional]\` marker; do not add a second dialect such as a blockquote or
|
|
1049
|
+
"Confidence: low". Intent is the most common thing code cannot tell you: a single
|
|
1050
|
+
\`export { app }\` does not prove a test seam was intended, and one carefully
|
|
1051
|
+
written expression does not prove a repository convention. Mark those
|
|
1052
|
+
\`[provisional]\` or leave them out.
|
|
1053
|
+
|
|
1054
|
+
If a fact cannot be established with reasonable effort, omit it. A short map
|
|
1055
|
+
that is true beats a long map that is partly guesswork.
|
|
1056
|
+
|
|
1057
|
+
# Keep it small
|
|
1058
|
+
|
|
1059
|
+
The context system exists to REDUCE context consumption. Never let the map grow
|
|
1060
|
+
into an encyclopedia. Never write:
|
|
1061
|
+
|
|
1062
|
+
- file inventories or directory listings
|
|
1063
|
+
- copied or paraphrased source code
|
|
1064
|
+
- large API descriptions
|
|
1065
|
+
- content duplicated from the project's own docs \u2014 link to them instead
|
|
1066
|
+
- tables, fenced code blocks, or copied measurements
|
|
1067
|
+
- generated documentation
|
|
1068
|
+
- exhaustive class or function lists
|
|
1069
|
+
- the same finding restated in several files \u2014 state a fact once, in the file
|
|
1070
|
+
that owns it, and let the others point at it
|
|
1071
|
+
- generic programming advice that would be true in any repository
|
|
1072
|
+
|
|
1073
|
+
Write conclusions, not inventories. Prefer a statement like "\`OrderService\` is
|
|
1074
|
+
the application boundary for order mutations" over a description of every
|
|
1075
|
+
method it exposes.
|
|
1076
|
+
|
|
1077
|
+
If a file grows past its budget, consolidate: merge overlapping entries, drop
|
|
1078
|
+
the lowest-value provisional ones, and link out to authoritative project docs.
|
|
1079
|
+
Never drop a binding constraint to make room.
|
|
1080
|
+
|
|
1081
|
+
# Review mode
|
|
1082
|
+
|
|
1083
|
+
Trigger: an explicit review (see the \`context-review\` command).
|
|
1084
|
+
|
|
1085
|
+
Audit the map files and the backlog for:
|
|
1086
|
+
|
|
1087
|
+
- schema \u2014 every entry has one marker, at most three lines, a pointer, and a
|
|
1088
|
+
\`verified\` date
|
|
1089
|
+
- budget \u2014 every file is within its non-blank-line cap
|
|
1090
|
+
- stale information \u2014 verify claims against the code and the cited page
|
|
1091
|
+
- contradictions between files, or between the map and a page \u2014 report them;
|
|
1092
|
+
mark \`[disputed]\`; never silently pick a side
|
|
1093
|
+
- dangling pointers \u2014 every cited path and \xA7section exists
|
|
1094
|
+
- unarchived content \u2014 an entry with no source in the tree; report it, keep it
|
|
1095
|
+
- restatement \u2014 a table, a fenced block, or a long verbatim span from a page
|
|
1096
|
+
- duplicated information \u2014 one canonical place per fact
|
|
1097
|
+
- missing canonical entry points for significant subsystems
|
|
1098
|
+
- unsupported assumptions \u2014 anything that is not \`[binding]\` or \`[disputed]\`
|
|
1099
|
+
should be \`[provisional]\`
|
|
1100
|
+
- backlog tasks that are finished but carry no date \u2014 the retention window
|
|
1101
|
+
cannot age those out, so they stay forever
|
|
1102
|
+
|
|
1103
|
+
Fix problems only in the files you own. Never modify source code or a project
|
|
1104
|
+
page during a review. Report what you found, what you fixed, what you left as
|
|
1105
|
+
\`[disputed]\`, and what you could not verify.
|
|
1106
|
+
|
|
1107
|
+
# Reporting
|
|
1108
|
+
|
|
1109
|
+
Finish with a short report:
|
|
1110
|
+
|
|
1111
|
+
- what you inspected
|
|
1112
|
+
- what you created or changed, and why it matters for future agents
|
|
1113
|
+
- what you deliberately left out
|
|
1114
|
+
- every map/page contradiction, with both citations; say you did not resolve it
|
|
1115
|
+
- any unarchived content that has no source in the tree
|
|
1116
|
+
- what the lifecycle removed and which file it came from
|
|
1117
|
+
- anything you found that contradicts the code or the existing docs
|
|
1118
|
+
`;
|
|
1119
|
+
|
|
753
1120
|
// src/agents.ts
|
|
754
1121
|
var POOLED_BASES = ["explore", "implement", "context"];
|
|
755
1122
|
var AGENTS = {
|
|
@@ -1160,250 +1527,7 @@ if the blocker is understanding rather than execution, report that instead.`,
|
|
|
1160
1527
|
"description": "Repository cartographer and memory maintainer. Bootstraps and incrementally maintains .opencode/context/ (architecture, contexts, conventions, workflows, decisions) as a small navigation map for future agents, and keeps .opencode/context/active.md \u2014 the active backlog of work in flight, grouped by week. Runs automatically after sessions that changed files; can also be invoked with /context-init, /context-update, /context-review. Never use it for general coding tasks.",
|
|
1161
1528
|
"mode": "subagent",
|
|
1162
1529
|
"temperature": 0.1,
|
|
1163
|
-
prompt:
|
|
1164
|
-
|
|
1165
|
-
You are the repository cartographer and memory maintainer. You are not a coding
|
|
1166
|
-
agent. You maintain exactly one thing: the repository context map in
|
|
1167
|
-
\`.opencode/context/\`.
|
|
1168
|
-
|
|
1169
|
-
The context map is a lightweight navigation index that future agents read
|
|
1170
|
-
before exploring. It exists to answer "what should I look at first?" \u2014 never
|
|
1171
|
-
"tell me everything about the repository". Source code is authoritative; the
|
|
1172
|
-
map is a derived index of it.
|
|
1173
|
-
|
|
1174
|
-
# Owned files
|
|
1175
|
-
|
|
1176
|
-
You own exactly these files and nothing else:
|
|
1177
|
-
|
|
1178
|
-
- \`.opencode/context/architecture.md\` \u2014 high-level architecture, major layers,
|
|
1179
|
-
boundaries, important entry points, dependency rules. Target 50\u2013150 lines.
|
|
1180
|
-
- \`.opencode/context/contexts.md\` \u2014 the major subsystems/modules. For each:
|
|
1181
|
-
purpose, canonical start-here files, boundaries, dependencies, and what must
|
|
1182
|
-
not be changed while working there. Target 50\u2013200 lines.
|
|
1183
|
-
- \`.opencode/context/conventions.md\` \u2014 repository-specific conventions:
|
|
1184
|
-
naming, patterns, error handling, testing, persistence, framework rules.
|
|
1185
|
-
Target 30\u2013150 lines.
|
|
1186
|
-
- \`.opencode/context/workflows.md\` \u2014 the few runtime flows worth tracing.
|
|
1187
|
-
Target 30\u2013150 lines.
|
|
1188
|
-
- \`.opencode/context/decisions.md\` \u2014 architectural decisions discovered
|
|
1189
|
-
during work that are not already recorded in the project's own decision
|
|
1190
|
-
log. Concise; variable length.
|
|
1191
|
-
- \`.opencode/context/active.md\` \u2014 the active backlog: work in flight, grouped
|
|
1192
|
-
by the week it was opened. Not part of the navigation map; it is described
|
|
1193
|
-
under "Task backlog" below.
|
|
1194
|
-
|
|
1195
|
-
# Current-state only
|
|
1196
|
-
|
|
1197
|
-
Every file you write states only the current arrangement and the reason it
|
|
1198
|
-
holds, in the present tense. No "used to", "previously", "before this",
|
|
1199
|
-
migration notes or deprecation narrative \u2014 history belongs in the project's
|
|
1200
|
-
changelog, not in your map. An absence that is true today is a fact worth
|
|
1201
|
-
stating, not a removal worth narrating. When a past state is worth keeping,
|
|
1202
|
-
record it briefly in \`changelog.md\` instead.
|
|
1203
|
-
|
|
1204
|
-
Never create, modify, or delete anything outside \`.opencode/context/\`. The
|
|
1205
|
-
\`edit\` permission enforces this. Do not attempt to work around it with bash.
|
|
1206
|
-
|
|
1207
|
-
To look for the map, use \`list\` or \`ls .opencode/context\`. Never use the
|
|
1208
|
-
\`glob\` tool for it: \`glob\` does not match hidden directories, so it reports
|
|
1209
|
-
"no files" even when the map exists.
|
|
1210
|
-
|
|
1211
|
-
# Bootstrap
|
|
1212
|
-
|
|
1213
|
-
Trigger: \`.opencode/context/\` does not exist or is empty.
|
|
1214
|
-
|
|
1215
|
-
Explore progressively and stop as soon as you have a usable map:
|
|
1216
|
-
|
|
1217
|
-
1. README files and any existing architecture or decision documentation.
|
|
1218
|
-
2. Project manifests (\`package.json\`, \`pyproject.toml\`, \`go.mod\`, \`Cargo.toml\`,
|
|
1219
|
-
\u2026) for entry points, workspaces, and dependency structure.
|
|
1220
|
-
3. The top-level directory structure \u2014 one or two levels. Name what each
|
|
1221
|
-
directory is for.
|
|
1222
|
-
4. Framework and build configuration (tsconfig, bundler config, CI, container
|
|
1223
|
-
files) to learn how things are assembled and run.
|
|
1224
|
-
5. Application entry points. Read them.
|
|
1225
|
-
6. Obvious domain / application / infrastructure boundaries and the
|
|
1226
|
-
interfaces between them.
|
|
1227
|
-
7. Only then inspect source selectively \u2014 the files that define the boundaries
|
|
1228
|
-
you found.
|
|
1229
|
-
|
|
1230
|
-
You are mapping a city, not auditing every house. Do not attempt to
|
|
1231
|
-
understand every file.
|
|
1232
|
-
|
|
1233
|
-
Then write the five files. Also create \`.opencode/context/active.md\` with its
|
|
1234
|
-
heading and no tasks. For each significant subsystem, give a short
|
|
1235
|
-
"Start here" list \u2014 at most about five canonical files, each with a one-phrase
|
|
1236
|
-
reason. This is the single most valuable thing you produce: where an agent
|
|
1237
|
-
should open its editor next.
|
|
1238
|
-
|
|
1239
|
-
# Incremental update
|
|
1240
|
-
|
|
1241
|
-
Trigger: \`.opencode/context/\` exists and work has just been done here.
|
|
1242
|
-
|
|
1243
|
-
1. Start from the files that changed, plus their immediate architectural
|
|
1244
|
-
neighborhood: what they import, who calls them, the boundary they sit on.
|
|
1245
|
-
2. Read only the context file(s) those changes could affect.
|
|
1246
|
-
3. Default to no edit. A changed file is not a changed fact: comments,
|
|
1247
|
-
formatting, renames of local variables, added tests, dependency bumps, and
|
|
1248
|
-
edits that stay inside one function rarely alter what the map should say.
|
|
1249
|
-
Edit only when the work introduced or revealed information that will
|
|
1250
|
-
materially help a future agent, such as:
|
|
1251
|
-
- a new architectural boundary or module
|
|
1252
|
-
- a new or moved canonical entry point
|
|
1253
|
-
- a changed workflow
|
|
1254
|
-
- a new repository convention
|
|
1255
|
-
- an architectural decision
|
|
1256
|
-
- a new dependency between subsystems
|
|
1257
|
-
- a renamed or relocated subsystem
|
|
1258
|
-
- an invariant not yet recorded in the map
|
|
1259
|
-
|
|
1260
|
-
Do not update documentation merely because source files changed. Do not
|
|
1261
|
-
rewrite files wholesale. Do not make timestamp-only edits. Do not add a fact
|
|
1262
|
-
that is already stated in another context file \u2014 one fact has one home, and
|
|
1263
|
-
the other files link to it. If nothing material changed, make no edits and
|
|
1264
|
-
report "no update needed".
|
|
1265
|
-
|
|
1266
|
-
Also keep \`.opencode/context/active.md\` current: record work that is in
|
|
1267
|
-
flight, and mark a task finished \u2014 with the date it finished \u2014 when it is.
|
|
1268
|
-
|
|
1269
|
-
# Task backlog
|
|
1270
|
-
|
|
1271
|
-
\`.opencode/context/active.md\` holds the work in flight. It is a rolling
|
|
1272
|
-
window, not a history: it answers "what is open, and what just finished".
|
|
1273
|
-
|
|
1274
|
-
Format \u2014 one line per task, under the ISO week the task was opened in:
|
|
1275
|
-
|
|
1276
|
-
## 2026-W43
|
|
1277
|
-
|
|
1278
|
-
- [ ] Ship the release notes \u2014 opened 2026-W43
|
|
1279
|
-
- [x] Fix the flaky test \u2014 opened 2026-W43, finished 2026-10-19
|
|
1280
|
-
|
|
1281
|
-
The rules:
|
|
1282
|
-
|
|
1283
|
-
- A task that is not marked \`[x]\` is OPEN. An open task is never removed,
|
|
1284
|
-
however old it is.
|
|
1285
|
-
- A task is FINISHED when it is marked \`[x]\` AND carries a \`finished\`
|
|
1286
|
-
date. Record the date it actually finished, as \`YYYY-MM-DD\`.
|
|
1287
|
-
- The week is \`YYYY-Www\`, Monday to Sunday. \`date +%G-W%V\` prints it.
|
|
1288
|
-
- A finished task is dropped once it is more than a week old. The plugin
|
|
1289
|
-
enforces that: record the finished task with its date and let the window
|
|
1290
|
-
close on its own. Do not delete old finished tasks by hand, and do not
|
|
1291
|
-
keep a finished task back to preserve it \u2014 the file is meant to stay
|
|
1292
|
-
small.
|
|
1293
|
-
- Never invent a task. Record work that actually happened, and mark a task
|
|
1294
|
-
finished only once it is finished.
|
|
1295
|
-
- Keep the file inside the window. If the backlog has grown past a handful
|
|
1296
|
-
of weeks, that is a signal to close finished tasks, not to add sections.
|
|
1297
|
-
|
|
1298
|
-
# Minimal exploration
|
|
1299
|
-
|
|
1300
|
-
Default shape:
|
|
1301
|
-
|
|
1302
|
-
repository structure
|
|
1303
|
-
\u2192 existing documentation
|
|
1304
|
-
\u2192 relevant entry points
|
|
1305
|
-
\u2192 relevant neighboring modules
|
|
1306
|
-
\u2192 deeper exploration only when necessary
|
|
1307
|
-
|
|
1308
|
-
Avoid:
|
|
1309
|
-
|
|
1310
|
-
repository
|
|
1311
|
-
\u2192 every directory
|
|
1312
|
-
\u2192 every file
|
|
1313
|
-
\u2192 every dependency
|
|
1314
|
-
\u2192 every caller
|
|
1315
|
-
\u2192 the whole codebase
|
|
1316
|
-
|
|
1317
|
-
Aim for minimum sufficient exploration. A few precise reads beat a broad
|
|
1318
|
-
sweep. When updating after a task, the changed files and their immediate
|
|
1319
|
-
neighborhood are almost always enough.
|
|
1320
|
-
|
|
1321
|
-
# Stay inside this project
|
|
1322
|
-
|
|
1323
|
-
The target repository is the only thing you are mapping. Everything outside its
|
|
1324
|
-
root is out of scope and is blocked:
|
|
1325
|
-
|
|
1326
|
-
- Do not read OpenCode's own configuration, prompts, plugins, session storage,
|
|
1327
|
-
or logs. They are not part of the project and hold no project facts.
|
|
1328
|
-
- Do not inspect other repositories, home directories, or system paths.
|
|
1329
|
-
- Do not treat a blocked tool call as a puzzle to solve. If a read or command
|
|
1330
|
-
is refused, use the project's own files instead and move on.
|
|
1331
|
-
- Do not search the filesystem root or use \`grep -r /\`.
|
|
1332
|
-
|
|
1333
|
-
# Source of truth
|
|
1334
|
-
|
|
1335
|
-
Source code wins. If the context map disagrees with the code:
|
|
1336
|
-
|
|
1337
|
-
1. Investigate the discrepancy.
|
|
1338
|
-
2. Trust the source.
|
|
1339
|
-
3. Correct the map.
|
|
1340
|
-
|
|
1341
|
-
Never propagate stale context. Never edit source code to match the map \u2014 you
|
|
1342
|
-
never edit source code at all.
|
|
1343
|
-
|
|
1344
|
-
# Confidence
|
|
1345
|
-
|
|
1346
|
-
Do not record assumptions as facts. If something is uncertain, mark it inline:
|
|
1347
|
-
|
|
1348
|
-
> Confidence: low \u2014 inferred from \`src/foo.ts\` naming only
|
|
1349
|
-
|
|
1350
|
-
Intent is the most common thing code cannot tell you. A single \`export { app }\`
|
|
1351
|
-
does not prove a test seam was intended, and one carefully written expression
|
|
1352
|
-
does not prove a repository convention. Mark those inferences or leave them
|
|
1353
|
-
out.
|
|
1354
|
-
|
|
1355
|
-
If a fact cannot be established with reasonable effort, omit it. A short map
|
|
1356
|
-
that is true beats a long map that is partly guesswork.
|
|
1357
|
-
|
|
1358
|
-
# Keep it small
|
|
1359
|
-
|
|
1360
|
-
The context system exists to REDUCE context consumption. Never let the map
|
|
1361
|
-
grow into an encyclopedia. Do not write:
|
|
1362
|
-
|
|
1363
|
-
- file inventories or directory listings
|
|
1364
|
-
- copied or paraphrased source code
|
|
1365
|
-
- large API descriptions
|
|
1366
|
-
- content duplicated from the project's own docs \u2014 link to them instead
|
|
1367
|
-
- generated documentation
|
|
1368
|
-
- exhaustive class or function lists
|
|
1369
|
-
- the same finding restated in several files \u2014 state a fact once, in the file
|
|
1370
|
-
that owns it, and let the others point at it
|
|
1371
|
-
- generic programming advice that would be true in any repository
|
|
1372
|
-
|
|
1373
|
-
Write conclusions, not inventories. Prefer a statement like
|
|
1374
|
-
"\`OrderService\` is the application boundary for order mutations" over a
|
|
1375
|
-
description of every method it exposes.
|
|
1376
|
-
|
|
1377
|
-
If a file grows past its target, consolidate: merge overlapping entries, drop
|
|
1378
|
-
the lowest-value ones, and link out to authoritative project docs.
|
|
1379
|
-
|
|
1380
|
-
# Review mode
|
|
1381
|
-
|
|
1382
|
-
Trigger: an explicit review (see the \`context-review\` command).
|
|
1383
|
-
|
|
1384
|
-
Audit the map files and the backlog for:
|
|
1385
|
-
|
|
1386
|
-
- stale information \u2014 verify claims against the code
|
|
1387
|
-
- contradictions between files, or between the map and the code
|
|
1388
|
-
- excessive size \u2014 consolidate toward the targets above
|
|
1389
|
-
- duplicated information \u2014 one canonical place per fact
|
|
1390
|
-
- missing canonical entry points for significant subsystems
|
|
1391
|
-
- unsupported assumptions \u2014 anything unmarked that you cannot verify
|
|
1392
|
-
- backlog tasks that are finished but carry no date \u2014 the retention window
|
|
1393
|
-
cannot age those out, so they stay forever
|
|
1394
|
-
|
|
1395
|
-
Fix problems only in the files you own. Never modify source code during a
|
|
1396
|
-
review. Report what you found, what you fixed, and what you could not
|
|
1397
|
-
verify.
|
|
1398
|
-
|
|
1399
|
-
# Reporting
|
|
1400
|
-
|
|
1401
|
-
Finish with a short report:
|
|
1402
|
-
|
|
1403
|
-
- what you inspected
|
|
1404
|
-
- what you created or changed, and why it matters for future agents
|
|
1405
|
-
- what you deliberately left out
|
|
1406
|
-
- anything you found that contradicts the code or the existing docs`,
|
|
1530
|
+
prompt: CARTOGRAPHER_PROMPT,
|
|
1407
1531
|
permission: {
|
|
1408
1532
|
"read": "allow",
|
|
1409
1533
|
"glob": "allow",
|
|
@@ -1624,11 +1748,14 @@ your system prompt.
|
|
|
1624
1748
|
Otherwise follow the bootstrap procedure in your system prompt: progressive
|
|
1625
1749
|
exploration (README \u2192 existing docs \u2192 manifests \u2192 top-level structure \u2192 build
|
|
1626
1750
|
and framework config \u2192 entry points \u2192 selective source), then write the five
|
|
1627
|
-
context files inside their
|
|
1628
|
-
|
|
1751
|
+
context files inside their line budgets, using the entry schema (provenance
|
|
1752
|
+
marker, rule of at most three lines, \`\u2014 See <path>[ \xA7Section] (verified
|
|
1753
|
+
YYYY-MM-DD)\`). Also create \`.opencode/context/active.md\` with its heading and no
|
|
1754
|
+
tasks.
|
|
1629
1755
|
|
|
1630
|
-
Remember: write conclusions, not inventories.
|
|
1631
|
-
documentation instead of
|
|
1756
|
+
Remember: write conclusions, not inventories. Point at the project's existing
|
|
1757
|
+
documentation instead of restating it. Run the drift pass and report a
|
|
1758
|
+
contradiction rather than silently choosing a side.
|
|
1632
1759
|
|
|
1633
1760
|
Report what you created or updated, what you deliberately left out, and
|
|
1634
1761
|
anything that contradicts the code or existing docs.`
|
|
@@ -1654,7 +1781,11 @@ or an invariant not yet recorded.
|
|
|
1654
1781
|
|
|
1655
1782
|
Do not edit documentation just because source files changed. No
|
|
1656
1783
|
timestamp-only edits. If nothing material changed, make no edits and report
|
|
1657
|
-
"no update needed"
|
|
1784
|
+
"no update needed".
|
|
1785
|
+
|
|
1786
|
+
Every new or changed entry uses the schema in your system prompt and stays
|
|
1787
|
+
under the file's line budget. If a file is already over budget, consolidate
|
|
1788
|
+
before adding.`
|
|
1658
1789
|
},
|
|
1659
1790
|
"context-review": {
|
|
1660
1791
|
description: "Audit .opencode/context/ for staleness, contradictions, excess size, duplication, and missing entry points.",
|
|
@@ -1662,20 +1793,29 @@ timestamp-only edits. If nothing material changed, make no edits and report
|
|
|
1662
1793
|
subtask: true,
|
|
1663
1794
|
template: `Context review.
|
|
1664
1795
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1796
|
+
Follow the review procedure and entry schema in your system prompt. Everything
|
|
1797
|
+
outside \`.opencode/context/\` is read-only for this run. Do not modify source
|
|
1798
|
+
code under any circumstances.
|
|
1667
1799
|
|
|
1668
1800
|
Audit the context files and the backlog for:
|
|
1669
1801
|
|
|
1670
|
-
-
|
|
1671
|
-
|
|
1672
|
-
-
|
|
1673
|
-
|
|
1802
|
+
- schema \u2014 every entry has one provenance marker, at most three lines, a
|
|
1803
|
+
pointer, and a \`(verified YYYY-MM-DD)\` date
|
|
1804
|
+
- budget \u2014 every file is within its non-blank-line cap; consolidate rather
|
|
1805
|
+
than exceed it
|
|
1806
|
+
- stale information \u2014 verify claims against the actual code and the cited page
|
|
1807
|
+
- contradictions between context files, or between the map and a page \u2014 mark
|
|
1808
|
+
\`[disputed]\` and report both sides; never silently prefer one
|
|
1809
|
+
- dangling pointers \u2014 every cited path and \xA7section must exist
|
|
1810
|
+
- unarchived content \u2014 an entry with no source in the tree; report it and keep
|
|
1811
|
+
it, because it may be the only copy
|
|
1812
|
+
- restatement \u2014 a table, a fenced block, or a long verbatim span copied from a
|
|
1813
|
+
page
|
|
1674
1814
|
- duplicated information \u2014 one canonical place per fact; link to the
|
|
1675
1815
|
project's own docs rather than restating them
|
|
1676
1816
|
- missing canonical entry points for significant subsystems
|
|
1677
|
-
- unsupported assumptions \u2014 anything
|
|
1678
|
-
|
|
1817
|
+
- unsupported assumptions \u2014 anything that is not \`[binding]\` or \`[disputed]\`
|
|
1818
|
+
should be \`[provisional]\`
|
|
1679
1819
|
- backlog tasks that are finished but carry no date \u2014 the retention window
|
|
1680
1820
|
cannot age those out, so they stay forever
|
|
1681
1821
|
|
|
@@ -1816,9 +1956,9 @@ function compareSemver(a, b) {
|
|
|
1816
1956
|
}
|
|
1817
1957
|
return 0;
|
|
1818
1958
|
}
|
|
1819
|
-
function readInstalledVersion(packageRoot, read = (
|
|
1959
|
+
function readInstalledVersion(packageRoot, read = (path5) => {
|
|
1820
1960
|
try {
|
|
1821
|
-
return readFileSync3(
|
|
1961
|
+
return readFileSync3(path5, "utf8");
|
|
1822
1962
|
} catch {
|
|
1823
1963
|
return void 0;
|
|
1824
1964
|
}
|
|
@@ -1876,17 +2016,17 @@ async function fetchLatest(fetchImpl = fetch, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
|
1876
2016
|
clearTimeout(timer);
|
|
1877
2017
|
}
|
|
1878
2018
|
}
|
|
1879
|
-
function readState(
|
|
2019
|
+
function readState(path5 = STATE_PATH) {
|
|
1880
2020
|
try {
|
|
1881
|
-
return JSON.parse(readFileSync3(
|
|
2021
|
+
return JSON.parse(readFileSync3(path5, "utf8"));
|
|
1882
2022
|
} catch {
|
|
1883
2023
|
return {};
|
|
1884
2024
|
}
|
|
1885
2025
|
}
|
|
1886
|
-
function writeState(state2,
|
|
2026
|
+
function writeState(state2, path5 = STATE_PATH) {
|
|
1887
2027
|
try {
|
|
1888
|
-
mkdirSync2(dirname2(
|
|
1889
|
-
writeFileSync2(
|
|
2028
|
+
mkdirSync2(dirname2(path5), { recursive: true });
|
|
2029
|
+
writeFileSync2(path5, JSON.stringify(state2));
|
|
1890
2030
|
} catch {
|
|
1891
2031
|
}
|
|
1892
2032
|
}
|
|
@@ -1899,39 +2039,39 @@ var isProcessAlive = (pid) => {
|
|
|
1899
2039
|
return error.code === "EPERM";
|
|
1900
2040
|
}
|
|
1901
2041
|
};
|
|
1902
|
-
function acquireLock(
|
|
2042
|
+
function acquireLock(path5 = LOCK_PATH, now = Date.now()) {
|
|
1903
2043
|
const claim = () => {
|
|
1904
|
-
const fd = openSync(
|
|
2044
|
+
const fd = openSync(path5, "wx");
|
|
1905
2045
|
writeSync(fd, String(process.pid));
|
|
1906
2046
|
closeSync(fd);
|
|
1907
2047
|
return () => {
|
|
1908
2048
|
try {
|
|
1909
|
-
unlinkSync2(
|
|
2049
|
+
unlinkSync2(path5);
|
|
1910
2050
|
} catch {
|
|
1911
2051
|
}
|
|
1912
2052
|
};
|
|
1913
2053
|
};
|
|
1914
2054
|
try {
|
|
1915
|
-
mkdirSync2(dirname2(
|
|
2055
|
+
mkdirSync2(dirname2(path5), { recursive: true });
|
|
1916
2056
|
return claim();
|
|
1917
2057
|
} catch {
|
|
1918
2058
|
let orphaned = false;
|
|
1919
2059
|
try {
|
|
1920
|
-
const pid = Number.parseInt(readFileSync3(
|
|
2060
|
+
const pid = Number.parseInt(readFileSync3(path5, "utf8").trim(), 10);
|
|
1921
2061
|
orphaned = !Number.isFinite(pid) || pid <= 0 || !isProcessAlive(pid);
|
|
1922
2062
|
} catch {
|
|
1923
2063
|
orphaned = true;
|
|
1924
2064
|
}
|
|
1925
2065
|
if (!orphaned) {
|
|
1926
2066
|
try {
|
|
1927
|
-
orphaned = now - statSync2(
|
|
2067
|
+
orphaned = now - statSync2(path5).mtimeMs > LOCK_STALE_MS;
|
|
1928
2068
|
} catch {
|
|
1929
2069
|
orphaned = true;
|
|
1930
2070
|
}
|
|
1931
2071
|
}
|
|
1932
2072
|
if (!orphaned) return void 0;
|
|
1933
2073
|
try {
|
|
1934
|
-
unlinkSync2(
|
|
2074
|
+
unlinkSync2(path5);
|
|
1935
2075
|
} catch {
|
|
1936
2076
|
return void 0;
|
|
1937
2077
|
}
|
|
@@ -2090,12 +2230,12 @@ var probeOne = async (client, opts, model, leftSeconds) => {
|
|
|
2090
2230
|
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2091
2231
|
const clock = opts.now ?? Date.now;
|
|
2092
2232
|
let timedOut = false;
|
|
2093
|
-
const deadline = new Promise((
|
|
2233
|
+
const deadline = new Promise((resolve4) => {
|
|
2094
2234
|
timer = setTimeout(() => {
|
|
2095
2235
|
timedOut = true;
|
|
2096
2236
|
void client.session.abort({ path: { id: created }, query: { directory } }).catch(() => {
|
|
2097
2237
|
});
|
|
2098
|
-
|
|
2238
|
+
resolve4(void 0);
|
|
2099
2239
|
}, timeoutMs);
|
|
2100
2240
|
});
|
|
2101
2241
|
const answer = client.session.prompt({
|
|
@@ -2823,9 +2963,9 @@ two ordered passes in slot order: pass 1 fills each slot to its weight, pass 2 t
|
|
|
2823
2963
|
});
|
|
2824
2964
|
|
|
2825
2965
|
// src/plugins/context-autoupdate.ts
|
|
2826
|
-
import { existsSync as
|
|
2966
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2827
2967
|
import { homedir as homedir3 } from "node:os";
|
|
2828
|
-
import * as
|
|
2968
|
+
import * as path3 from "node:path";
|
|
2829
2969
|
|
|
2830
2970
|
// src/lib/active-backlog.ts
|
|
2831
2971
|
var ACTIVE_BACKLOG_FILE = "active.md";
|
|
@@ -2927,6 +3067,494 @@ var normalizeBacklog = (text, now = Date.now()) => {
|
|
|
2927
3067
|
return { text: renderBacklog(kept), removed };
|
|
2928
3068
|
};
|
|
2929
3069
|
|
|
3070
|
+
// src/lib/map-schema.ts
|
|
3071
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3072
|
+
import * as path from "node:path";
|
|
3073
|
+
var PROVISIONAL_DAYS = 30;
|
|
3074
|
+
var MAP_SPECS = [
|
|
3075
|
+
{
|
|
3076
|
+
file: "architecture.md",
|
|
3077
|
+
charter: "cross-subsystem structure: layers, boundaries, entry points, dependency rules",
|
|
3078
|
+
budget: 150,
|
|
3079
|
+
minEntries: 4,
|
|
3080
|
+
kind: "entries",
|
|
3081
|
+
provisionalDays: PROVISIONAL_DAYS,
|
|
3082
|
+
sectionRequired: true
|
|
3083
|
+
},
|
|
3084
|
+
{
|
|
3085
|
+
file: "contexts.md",
|
|
3086
|
+
charter: "subsystem boundaries: purpose, start-here files, what must not change",
|
|
3087
|
+
budget: 200,
|
|
3088
|
+
minEntries: 3,
|
|
3089
|
+
kind: "entries",
|
|
3090
|
+
provisionalDays: PROVISIONAL_DAYS,
|
|
3091
|
+
sectionRequired: true
|
|
3092
|
+
},
|
|
3093
|
+
{
|
|
3094
|
+
file: "conventions.md",
|
|
3095
|
+
charter: "rules to follow before writing code",
|
|
3096
|
+
budget: 150,
|
|
3097
|
+
minEntries: 3,
|
|
3098
|
+
kind: "entries",
|
|
3099
|
+
provisionalDays: PROVISIONAL_DAYS,
|
|
3100
|
+
sectionRequired: true
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
file: "workflows.md",
|
|
3104
|
+
charter: "runtime behaviour worth tracing",
|
|
3105
|
+
budget: 150,
|
|
3106
|
+
minEntries: 2,
|
|
3107
|
+
kind: "entries",
|
|
3108
|
+
provisionalDays: PROVISIONAL_DAYS,
|
|
3109
|
+
sectionRequired: true
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
file: "decisions.md",
|
|
3113
|
+
charter: "architectural constraints that bind future work",
|
|
3114
|
+
budget: 120,
|
|
3115
|
+
minEntries: 1,
|
|
3116
|
+
kind: "entries",
|
|
3117
|
+
provisionalDays: PROVISIONAL_DAYS,
|
|
3118
|
+
sectionRequired: true
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
file: ACTIVE_BACKLOG_FILE,
|
|
3122
|
+
charter: "work in flight, grouped by the ISO week it was opened",
|
|
3123
|
+
budget: 60,
|
|
3124
|
+
minEntries: 0,
|
|
3125
|
+
kind: "backlog",
|
|
3126
|
+
provisionalDays: PROVISIONAL_DAYS,
|
|
3127
|
+
sectionRequired: false
|
|
3128
|
+
}
|
|
3129
|
+
];
|
|
3130
|
+
var MAP_SPEC_BY_FILE = new Map(MAP_SPECS.map((spec) => [spec.file, spec]));
|
|
3131
|
+
var MAP_FILES = MAP_SPECS.map((spec) => spec.file);
|
|
3132
|
+
var NAVIGATION_FILES = MAP_SPECS.filter((spec) => spec.kind === "entries").map((spec) => spec.file);
|
|
3133
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
3134
|
+
var ENTRY_START = /^([-*+])\s+\[(binding|provisional|disputed)\]\s+(.*)$/i;
|
|
3135
|
+
var CONTINUATION = /^\s{2,}\S/;
|
|
3136
|
+
var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
|
3137
|
+
var FENCE = /^\s*(?:```|~~~)/;
|
|
3138
|
+
var TABLE_ROW = /^\s*\|.*\|\s*$/;
|
|
3139
|
+
var TABLE_SEPARATOR = /^\s*\|[\s:|-]*-{3,}[\s:|-]*\|\s*$/;
|
|
3140
|
+
var VERIFIED = /\(verified\s+(\d{4}-\d{2}-\d{2})\)\s*$/i;
|
|
3141
|
+
var POINTER = /(?:—|–|→|->)\s*See\s+`([^`\n]+)`(?:\s*§\s*([^()\n]+?))?(?:\s*\(verified\s+(\d{4}-\d{2}-\d{2})\))?\s*$/i;
|
|
3142
|
+
var DECISION_RECORD = /^\s*(?:#{1,6}\s*)?(?:\*\*)?(Context|Decision|Consequences|Alternatives|Status|Trade-?offs|Rationale)(?:\*\*)?\s*(?::|$)/im;
|
|
3143
|
+
var ARCHIVE_LANGUAGE = /\b(?:archive\s+of\s+record|this\s+(?:is|file\s+is|map\s+is)\s+the\s+only\s+(?:copy|record)|the\s+only\s+place\s+[^.\n]{0,80}?\b(?:remains|survives)\b|the\s+only\s+(?:copy|record)\s+[^.\n]{0,80}?\b(?:remains|survives|is\s+kept)\b|(?:survives|remains|is\s+kept)\s+only\s+here|no\s+longer\s+exists\s+(?:in|anywhere)|exists\s+only\s+(?:in|here))\b/i;
|
|
3144
|
+
var isCalendarDate2 = (value) => {
|
|
3145
|
+
const at = Date.parse(`${value}T00:00:00Z`);
|
|
3146
|
+
return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;
|
|
3147
|
+
};
|
|
3148
|
+
var normalizeHeading = (value) => value.toLowerCase().replace(/[`*_]/g, "").replace(/^\s*\d+(?:\.\d+)*[.)]?\s*/, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
3149
|
+
var normalizeRule = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
3150
|
+
var truncate = (value, max = 90) => value.length <= max ? value : `${value.slice(0, max - 1)}\u2026`;
|
|
3151
|
+
var parseMapEntries = (text) => {
|
|
3152
|
+
const lines = text.split(/\r?\n/);
|
|
3153
|
+
const entries = [];
|
|
3154
|
+
let inFence = false;
|
|
3155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3156
|
+
if (FENCE.test(lines[i])) {
|
|
3157
|
+
inFence = !inFence;
|
|
3158
|
+
continue;
|
|
3159
|
+
}
|
|
3160
|
+
if (inFence) continue;
|
|
3161
|
+
const start = ENTRY_START.exec(lines[i]);
|
|
3162
|
+
if (!start) continue;
|
|
3163
|
+
const bodyLines = [start[3]];
|
|
3164
|
+
let j = i + 1;
|
|
3165
|
+
while (j < lines.length && CONTINUATION.test(lines[j]) && !FENCE.test(lines[j])) {
|
|
3166
|
+
bodyLines.push(lines[j].trim());
|
|
3167
|
+
j++;
|
|
3168
|
+
}
|
|
3169
|
+
const body = bodyLines.join("\n");
|
|
3170
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
3171
|
+
const pointerMatch = POINTER.exec(flat);
|
|
3172
|
+
const verifiedMatch = VERIFIED.exec(flat);
|
|
3173
|
+
const pointer = pointerMatch?.[1]?.trim();
|
|
3174
|
+
const section = pointerMatch?.[2]?.trim() || void 0;
|
|
3175
|
+
const verified = pointerMatch?.[3] ?? verifiedMatch?.[1];
|
|
3176
|
+
const rule = flat.replace(POINTER, "").replace(VERIFIED, "").trim();
|
|
3177
|
+
entries.push({
|
|
3178
|
+
startLine: i,
|
|
3179
|
+
endLine: j,
|
|
3180
|
+
marker: start[2].toLowerCase(),
|
|
3181
|
+
body,
|
|
3182
|
+
rule,
|
|
3183
|
+
lineCount: bodyLines.filter((line) => line.trim()).length,
|
|
3184
|
+
...pointer ? { pointer } : {},
|
|
3185
|
+
...section ? { section } : {},
|
|
3186
|
+
...verified ? { verified } : {}
|
|
3187
|
+
});
|
|
3188
|
+
i = j - 1;
|
|
3189
|
+
}
|
|
3190
|
+
return entries;
|
|
3191
|
+
};
|
|
3192
|
+
var stripLineSuffix = (pointer) => pointer.replace(/[\\/]$/, "").replace(/(?::\d+(?:-\d+)?|#L\d+(?:-L?\d+)?)$/i, "");
|
|
3193
|
+
var headingOf = (line) => {
|
|
3194
|
+
const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
3195
|
+
if (!match) return void 0;
|
|
3196
|
+
return { level: match[1].length, text: match[2] };
|
|
3197
|
+
};
|
|
3198
|
+
var sectionText = (text, section) => {
|
|
3199
|
+
const lines = text.split(/\r?\n/);
|
|
3200
|
+
const wanted = normalizeHeading(section);
|
|
3201
|
+
if (!wanted) return void 0;
|
|
3202
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3203
|
+
const heading = headingOf(lines[i]);
|
|
3204
|
+
if (!heading) continue;
|
|
3205
|
+
const normalized = normalizeHeading(heading.text);
|
|
3206
|
+
if (!(normalized === wanted || normalized.includes(wanted) || wanted.includes(normalized))) continue;
|
|
3207
|
+
let end = lines.length;
|
|
3208
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3209
|
+
const next = headingOf(lines[j]);
|
|
3210
|
+
if (next && next.level <= heading.level) {
|
|
3211
|
+
end = j;
|
|
3212
|
+
break;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
return lines.slice(i + 1, end).join("\n");
|
|
3216
|
+
}
|
|
3217
|
+
return void 0;
|
|
3218
|
+
};
|
|
3219
|
+
var resolvePointer = (root, pointer, section) => {
|
|
3220
|
+
const cleaned = stripLineSuffix(pointer.trim().replace(/^`|`$/g, ""));
|
|
3221
|
+
if (!cleaned) return { exists: false };
|
|
3222
|
+
const absolute = path.resolve(root, cleaned);
|
|
3223
|
+
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
3224
|
+
if (absolute !== root && !absolute.startsWith(rootWithSep)) return { exists: false };
|
|
3225
|
+
let stat;
|
|
3226
|
+
try {
|
|
3227
|
+
stat = statSync3(absolute);
|
|
3228
|
+
} catch {
|
|
3229
|
+
return { exists: false };
|
|
3230
|
+
}
|
|
3231
|
+
if (!stat.isFile()) return { exists: false };
|
|
3232
|
+
let text;
|
|
3233
|
+
try {
|
|
3234
|
+
if (stat.size <= 512 * 1024) text = readFileSync4(absolute, "utf8");
|
|
3235
|
+
} catch {
|
|
3236
|
+
text = void 0;
|
|
3237
|
+
}
|
|
3238
|
+
if (section && text !== void 0) {
|
|
3239
|
+
const found = sectionText(text, section);
|
|
3240
|
+
return {
|
|
3241
|
+
absolute,
|
|
3242
|
+
exists: true,
|
|
3243
|
+
mtimeMs: stat.mtimeMs,
|
|
3244
|
+
sectionFound: found !== void 0,
|
|
3245
|
+
...found !== void 0 ? { sectionText: found } : {},
|
|
3246
|
+
sourceText: found ?? text
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
return { absolute, exists: true, mtimeMs: stat.mtimeMs, ...text !== void 0 ? { sourceText: text } : {} };
|
|
3250
|
+
};
|
|
3251
|
+
var words = (value) => value.toLowerCase().replace(/[`*_[\]()]/g, " ").split(/[^a-z0-9]+/).filter((word) => word.length > 0);
|
|
3252
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
3253
|
+
"the",
|
|
3254
|
+
"and",
|
|
3255
|
+
"for",
|
|
3256
|
+
"with",
|
|
3257
|
+
"that",
|
|
3258
|
+
"this",
|
|
3259
|
+
"from",
|
|
3260
|
+
"are",
|
|
3261
|
+
"was",
|
|
3262
|
+
"were",
|
|
3263
|
+
"has",
|
|
3264
|
+
"have",
|
|
3265
|
+
"had",
|
|
3266
|
+
"not",
|
|
3267
|
+
"never",
|
|
3268
|
+
"none",
|
|
3269
|
+
"into",
|
|
3270
|
+
"onto",
|
|
3271
|
+
"than",
|
|
3272
|
+
"then",
|
|
3273
|
+
"when",
|
|
3274
|
+
"where",
|
|
3275
|
+
"which",
|
|
3276
|
+
"what",
|
|
3277
|
+
"who",
|
|
3278
|
+
"will",
|
|
3279
|
+
"would",
|
|
3280
|
+
"can",
|
|
3281
|
+
"could",
|
|
3282
|
+
"must",
|
|
3283
|
+
"may",
|
|
3284
|
+
"might",
|
|
3285
|
+
"its",
|
|
3286
|
+
"their",
|
|
3287
|
+
"your",
|
|
3288
|
+
"our",
|
|
3289
|
+
"you",
|
|
3290
|
+
"they",
|
|
3291
|
+
"them",
|
|
3292
|
+
"there",
|
|
3293
|
+
"here",
|
|
3294
|
+
"only",
|
|
3295
|
+
"also",
|
|
3296
|
+
"but",
|
|
3297
|
+
"all",
|
|
3298
|
+
"any",
|
|
3299
|
+
"each",
|
|
3300
|
+
"every",
|
|
3301
|
+
"more",
|
|
3302
|
+
"most",
|
|
3303
|
+
"some",
|
|
3304
|
+
"such",
|
|
3305
|
+
"one",
|
|
3306
|
+
"two",
|
|
3307
|
+
"use",
|
|
3308
|
+
"used",
|
|
3309
|
+
"using",
|
|
3310
|
+
"via",
|
|
3311
|
+
"per",
|
|
3312
|
+
"out",
|
|
3313
|
+
"own"
|
|
3314
|
+
]);
|
|
3315
|
+
var contentWords = (value) => words(value).filter((word) => word.length >= 4 && !STOPWORDS.has(word));
|
|
3316
|
+
var ngrams = (values, size) => {
|
|
3317
|
+
const out = /* @__PURE__ */ new Set();
|
|
3318
|
+
for (let i = 0; i + size <= values.length; i++) out.add(values.slice(i, i + size).join(" "));
|
|
3319
|
+
return out;
|
|
3320
|
+
};
|
|
3321
|
+
var hasNegation = (value) => /\b(?:not|never|none|no\s+longer|refuted|unsupported|absent|missing|removed|without)\b/i.test(value);
|
|
3322
|
+
var STATE_AUXILIARY = /\b(?:is|are|was|were|has|have|had|does|do|can|could)\b/i;
|
|
3323
|
+
var numbersIn = (value) => (value.match(/\b\d+(?:[.,]\d+)?\b/g) ?? []).map((number) => number.replace(/,/g, ""));
|
|
3324
|
+
var sharesVerbatimRun = (rule, source) => {
|
|
3325
|
+
const entryWords = words(rule);
|
|
3326
|
+
if (entryWords.length < 12) return false;
|
|
3327
|
+
const sourceGrams = ngrams(words(source), 12);
|
|
3328
|
+
for (const gram of ngrams(entryWords, 12)) if (sourceGrams.has(gram)) return true;
|
|
3329
|
+
return false;
|
|
3330
|
+
};
|
|
3331
|
+
var possibleNegationConflict = (rule, source) => {
|
|
3332
|
+
if (hasNegation(rule)) return void 0;
|
|
3333
|
+
const entryWords = new Set(contentWords(rule));
|
|
3334
|
+
if (entryWords.size < 3) return void 0;
|
|
3335
|
+
const sentences = source.split(/(?:[.!?]\s+|\n+)/);
|
|
3336
|
+
for (const sentence of sentences) {
|
|
3337
|
+
if (!hasNegation(sentence) || !STATE_AUXILIARY.test(sentence)) continue;
|
|
3338
|
+
const sentenceWords = new Set(contentWords(sentence));
|
|
3339
|
+
if (sentenceWords.size === 0) continue;
|
|
3340
|
+
let overlap = 0;
|
|
3341
|
+
for (const word of entryWords) if (sentenceWords.has(word)) overlap++;
|
|
3342
|
+
const ratio = overlap / Math.min(entryWords.size, sentenceWords.size);
|
|
3343
|
+
if (overlap >= 3 && ratio >= 0.5) return truncate(sentence.trim());
|
|
3344
|
+
}
|
|
3345
|
+
return void 0;
|
|
3346
|
+
};
|
|
3347
|
+
var invalidNumbers = (rule, source) => {
|
|
3348
|
+
const claimed = [...new Set(numbersIn(rule))];
|
|
3349
|
+
if (claimed.length === 0) return [];
|
|
3350
|
+
const sourceNumbers = new Set(numbersIn(source));
|
|
3351
|
+
return claimed.filter((number) => !sourceNumbers.has(number));
|
|
3352
|
+
};
|
|
3353
|
+
var validateMapText = (spec, text, options) => {
|
|
3354
|
+
const violations = [];
|
|
3355
|
+
const lines = text.split(/\r?\n/);
|
|
3356
|
+
const nonBlank = lines.filter((line) => line.trim()).length;
|
|
3357
|
+
const push = (kind, detail, line) => violations.push({ file: spec.file, kind, detail, ...line !== void 0 ? { line: line + 1 } : {} });
|
|
3358
|
+
if (nonBlank === 0) push("charter", `empty file; charter is ${spec.charter}`);
|
|
3359
|
+
if (nonBlank > spec.budget) {
|
|
3360
|
+
push("budget", `${nonBlank} non-blank lines exceeds the ${spec.budget}-line budget; consolidate, do not pad`);
|
|
3361
|
+
}
|
|
3362
|
+
if (spec.kind === "backlog") {
|
|
3363
|
+
if (!text.startsWith("# ")) push("charter", "active backlog must start with a top-level heading");
|
|
3364
|
+
return { spec, lines: nonBlank, entries: [], violations };
|
|
3365
|
+
}
|
|
3366
|
+
const entries = parseMapEntries(text);
|
|
3367
|
+
let inFence = false;
|
|
3368
|
+
let tableReported = false;
|
|
3369
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3370
|
+
if (FENCE.test(lines[i])) {
|
|
3371
|
+
if (!inFence) push("restatement", "fenced code block; the map cites the file that owns the code, it does not copy it", i);
|
|
3372
|
+
inFence = !inFence;
|
|
3373
|
+
continue;
|
|
3374
|
+
}
|
|
3375
|
+
if (!inFence && !tableReported && (TABLE_ROW.test(lines[i]) || TABLE_SEPARATOR.test(lines[i]))) {
|
|
3376
|
+
push("restatement", "table; a table belongs on the page that owns it, not in the map", i);
|
|
3377
|
+
tableReported = true;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
const record = DECISION_RECORD.exec(text);
|
|
3381
|
+
if (record) {
|
|
3382
|
+
push("schema", `decision-record block (${record[1]}); state only the operative constraint and its pointer`);
|
|
3383
|
+
}
|
|
3384
|
+
if (ARCHIVE_LANGUAGE.test(text)) {
|
|
3385
|
+
push("archive", "the map claims to be an archive or the only copy; derived content must never be the archive of record");
|
|
3386
|
+
}
|
|
3387
|
+
const entryStarts = new Set(entries.map((entry) => entry.startLine));
|
|
3388
|
+
inFence = false;
|
|
3389
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3390
|
+
if (FENCE.test(lines[i])) {
|
|
3391
|
+
inFence = !inFence;
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
if (!inFence && LIST_ITEM.test(lines[i]) && !entryStarts.has(i)) {
|
|
3395
|
+
push("schema", "unmarked list item; every entry starts with - [binding], - [provisional] or - [disputed]", i);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
let binding = 0;
|
|
3399
|
+
for (const entry of entries) {
|
|
3400
|
+
const line = entry.startLine;
|
|
3401
|
+
if (entry.marker === "binding") binding++;
|
|
3402
|
+
if (entry.lineCount > 3) {
|
|
3403
|
+
push("schema", `entry is ${entry.lineCount} lines; the rule must fit in one to three`, line);
|
|
3404
|
+
}
|
|
3405
|
+
if (!entry.verified) {
|
|
3406
|
+
push("schema", "entry has no (verified YYYY-MM-DD); the lifecycle and drift checks need it", line);
|
|
3407
|
+
} else if (!isCalendarDate2(entry.verified)) {
|
|
3408
|
+
push("schema", `verified date ${entry.verified} is not a real calendar date`, line);
|
|
3409
|
+
} else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) + DAY_MS2) {
|
|
3410
|
+
push("schema", `verified date ${entry.verified} is more than a day in the future`, line);
|
|
3411
|
+
}
|
|
3412
|
+
if (!entry.pointer) {
|
|
3413
|
+
push("schema", "entry has no `\u2014 See <path>` pointer; a fact with no home is not a map entry", line);
|
|
3414
|
+
push("unarchived", "content has no resolvable source in the tree; report it, do not emit it as derived", line);
|
|
3415
|
+
continue;
|
|
3416
|
+
}
|
|
3417
|
+
if (spec.sectionRequired && /\.md$/i.test(stripLineSuffix(entry.pointer)) && !entry.section) {
|
|
3418
|
+
push("schema", `page citation \`${entry.pointer}\` needs a \xA7section; a page is cited, not restated`, line);
|
|
3419
|
+
}
|
|
3420
|
+
const resolved = resolvePointer(options.root, entry.pointer, entry.section);
|
|
3421
|
+
if (!resolved.exists) {
|
|
3422
|
+
push("dangling", `cites \`${entry.pointer}\`, which does not exist in the tree`, line);
|
|
3423
|
+
push("unarchived", `no source for \`${entry.pointer}\`; keep the text and report it, never delete it`, line);
|
|
3424
|
+
continue;
|
|
3425
|
+
}
|
|
3426
|
+
if (entry.section && resolved.sectionFound === false) {
|
|
3427
|
+
push("drift", `cites \xA7${entry.section} in \`${entry.pointer}\`, but that section no longer exists`, line);
|
|
3428
|
+
continue;
|
|
3429
|
+
}
|
|
3430
|
+
if (resolved.mtimeMs !== void 0 && entry.verified) {
|
|
3431
|
+
const modified = new Date(resolved.mtimeMs).toISOString().slice(0, 10);
|
|
3432
|
+
if (modified > entry.verified) {
|
|
3433
|
+
push("drift", `\`${entry.pointer}\` changed on ${modified}, after this entry was verified on ${entry.verified}; re-verify it`, line);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
if (resolved.sourceText !== void 0) {
|
|
3437
|
+
const missing = invalidNumbers(entry.rule, resolved.sourceText);
|
|
3438
|
+
if (missing.length > 0) {
|
|
3439
|
+
push("drift", `numeric claim(s) ${missing.join(", ")} not found in \`${entry.pointer}\`; the map and the source may disagree`, line);
|
|
3440
|
+
}
|
|
3441
|
+
if (sharesVerbatimRun(entry.rule, resolved.sourceText)) {
|
|
3442
|
+
push("restatement", `contains a long verbatim span from \`${entry.pointer}\`; cite the section instead`, line);
|
|
3443
|
+
}
|
|
3444
|
+
const conflict = possibleNegationConflict(entry.rule, resolved.sourceText);
|
|
3445
|
+
if (conflict) {
|
|
3446
|
+
push("drift", `possible contradiction with \`${entry.pointer}\`: "${conflict}"; verify, do not silently overwrite either side`, line);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
if (entries.length < spec.minEntries) {
|
|
3451
|
+
push("charter", `${entries.length} entr${entries.length === 1 ? "y" : "ies"} is degenerate for ${spec.charter}`);
|
|
3452
|
+
} else if (entries.length > 0 && binding === 0) {
|
|
3453
|
+
push("charter", "every entry is provisional or disputed; a navigation file must carry at least one binding constraint");
|
|
3454
|
+
}
|
|
3455
|
+
return { spec, lines: nonBlank, entries, violations };
|
|
3456
|
+
};
|
|
3457
|
+
var normalizeMapText = (spec, text, options = {}) => {
|
|
3458
|
+
const now = options.now ?? Date.now();
|
|
3459
|
+
if (spec.kind === "backlog") {
|
|
3460
|
+
const result = normalizeBacklog(text, now);
|
|
3461
|
+
return {
|
|
3462
|
+
text: result.text,
|
|
3463
|
+
removed: result.removed.map((title) => ({ line: 0, text: title })),
|
|
3464
|
+
kept: 0
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
const entries = parseMapEntries(text);
|
|
3468
|
+
const drop = /* @__PURE__ */ new Set();
|
|
3469
|
+
const removed = [];
|
|
3470
|
+
for (const entry of entries) {
|
|
3471
|
+
if (entry.marker !== "provisional") continue;
|
|
3472
|
+
if (!entry.verified || !entry.pointer) continue;
|
|
3473
|
+
if (options.canResolve && !options.canResolve(entry.pointer, entry.section)) continue;
|
|
3474
|
+
const at = Date.parse(`${entry.verified}T00:00:00Z`);
|
|
3475
|
+
if (!Number.isFinite(at)) continue;
|
|
3476
|
+
if (now - at <= spec.provisionalDays * DAY_MS2) continue;
|
|
3477
|
+
for (let i = entry.startLine; i < entry.endLine; i++) drop.add(i);
|
|
3478
|
+
removed.push({ line: entry.startLine + 1, text: truncate(entry.rule) });
|
|
3479
|
+
}
|
|
3480
|
+
if (removed.length === 0) return { text, removed, kept: entries.length };
|
|
3481
|
+
const keptLines = text.split(/\r?\n/).filter((_, index) => !drop.has(index));
|
|
3482
|
+
const compact = keptLines.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
3483
|
+
const out = compact.endsWith("\n") ? compact : `${compact}
|
|
3484
|
+
`;
|
|
3485
|
+
return { text: out, removed, kept: entries.length - removed.length };
|
|
3486
|
+
};
|
|
3487
|
+
var maintainMap = (root, mapDir, now = Date.now()) => {
|
|
3488
|
+
const files = [];
|
|
3489
|
+
let removedTotal = 0;
|
|
3490
|
+
for (const spec of MAP_SPECS) {
|
|
3491
|
+
const absolute = path.join(mapDir, spec.file);
|
|
3492
|
+
if (!existsSync2(absolute)) continue;
|
|
3493
|
+
let before;
|
|
3494
|
+
try {
|
|
3495
|
+
before = readFileSync4(absolute, "utf8");
|
|
3496
|
+
} catch {
|
|
3497
|
+
continue;
|
|
3498
|
+
}
|
|
3499
|
+
const canResolve = (pointer, section) => resolvePointer(root, pointer, section).exists;
|
|
3500
|
+
const result = normalizeMapText(spec, before, { now, canResolve });
|
|
3501
|
+
const changed = result.text !== before;
|
|
3502
|
+
if (changed) writeFileSync3(absolute, result.text);
|
|
3503
|
+
if (result.removed.length > 0) {
|
|
3504
|
+
removedTotal += result.removed.length;
|
|
3505
|
+
files.push({ file: spec.file, changed, removed: result.removed });
|
|
3506
|
+
} else if (changed) {
|
|
3507
|
+
files.push({ file: spec.file, changed, removed: [] });
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
return { files, removedTotal };
|
|
3511
|
+
};
|
|
3512
|
+
var auditMap = (root, mapDir, now = Date.now()) => {
|
|
3513
|
+
const files = [];
|
|
3514
|
+
const violations = [];
|
|
3515
|
+
const rulesSeen = /* @__PURE__ */ new Map();
|
|
3516
|
+
for (const spec of MAP_SPECS) {
|
|
3517
|
+
const absolute = path.join(mapDir, spec.file);
|
|
3518
|
+
if (!existsSync2(absolute)) {
|
|
3519
|
+
const violation = { file: spec.file, kind: "missing-file", detail: `missing; charter is ${spec.charter}` };
|
|
3520
|
+
violations.push(violation);
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
let text;
|
|
3524
|
+
try {
|
|
3525
|
+
text = readFileSync4(absolute, "utf8");
|
|
3526
|
+
} catch {
|
|
3527
|
+
violations.push({ file: spec.file, kind: "missing-file", detail: "unreadable" });
|
|
3528
|
+
continue;
|
|
3529
|
+
}
|
|
3530
|
+
const result = validateMapText(spec, text, { root, now });
|
|
3531
|
+
files.push(result);
|
|
3532
|
+
violations.push(...result.violations);
|
|
3533
|
+
if (spec.kind === "entries") {
|
|
3534
|
+
for (const entry of result.entries) {
|
|
3535
|
+
const key = normalizeRule(entry.rule);
|
|
3536
|
+
if (key.length < 12) continue;
|
|
3537
|
+
const home = rulesSeen.get(key);
|
|
3538
|
+
if (home !== void 0 && home !== spec.file) {
|
|
3539
|
+
violations.push({
|
|
3540
|
+
file: spec.file,
|
|
3541
|
+
line: entry.startLine + 1,
|
|
3542
|
+
kind: "duplicate",
|
|
3543
|
+
detail: `same rule is already stated in ${home}; one fact has one home`
|
|
3544
|
+
});
|
|
3545
|
+
} else if (home === void 0) {
|
|
3546
|
+
rulesSeen.set(key, spec.file);
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
return { ok: violations.length === 0, files, violations };
|
|
3552
|
+
};
|
|
3553
|
+
var formatViolations = (violations, limit = 20) => violations.slice(0, limit).map((violation) => {
|
|
3554
|
+
const where = violation.line !== void 0 ? `${violation.file}:${violation.line}` : violation.file;
|
|
3555
|
+
return `${where} [${violation.kind}] ${violation.detail}`;
|
|
3556
|
+
});
|
|
3557
|
+
|
|
2930
3558
|
// src/lib/jsonc.ts
|
|
2931
3559
|
var stripJsonc = (raw) => {
|
|
2932
3560
|
let out = "";
|
|
@@ -2975,12 +3603,12 @@ var parseJsonc = (raw) => {
|
|
|
2975
3603
|
};
|
|
2976
3604
|
|
|
2977
3605
|
// src/lib/writer-rule.ts
|
|
2978
|
-
import { readFileSync as
|
|
2979
|
-
import * as
|
|
2980
|
-
var CONTEXT_DIR =
|
|
3606
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3607
|
+
import * as path2 from "node:path";
|
|
3608
|
+
var CONTEXT_DIR = path2.join(".opencode", "context");
|
|
2981
3609
|
var WRITER = "context";
|
|
2982
3610
|
var INSTRUCTIONS_WRITER = AGENT_INSTRUCTIONS_WRITER;
|
|
2983
|
-
var toPosix = (value) => value.split(
|
|
3611
|
+
var toPosix = (value) => value.split(path2.sep).join("/");
|
|
2984
3612
|
var compilePattern = (pattern) => {
|
|
2985
3613
|
const escaped = toPosix(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2986
3614
|
return new RegExp(`^${escaped}$`, "s");
|
|
@@ -3005,17 +3633,17 @@ var readAgents = (raw) => {
|
|
|
3005
3633
|
};
|
|
3006
3634
|
var permissionOf = (agent) => agent?.permission?.edit;
|
|
3007
3635
|
var evaluatedForm = (mapDir, projectRoot) => {
|
|
3008
|
-
const absolute = toPosix(
|
|
3009
|
-
return projectRoot ? toPosix(
|
|
3636
|
+
const absolute = toPosix(path2.join(mapDir, "architecture.md"));
|
|
3637
|
+
return projectRoot ? toPosix(path2.relative(projectRoot, absolute)) : absolute;
|
|
3010
3638
|
};
|
|
3011
3639
|
var agentsForm = (mapDir, projectRoot) => {
|
|
3012
|
-
const absolute = toPosix(
|
|
3013
|
-
return projectRoot ? toPosix(
|
|
3640
|
+
const absolute = toPosix(path2.join(mapDir, "..", "..", "AGENTS.md"));
|
|
3641
|
+
return projectRoot ? toPosix(path2.relative(projectRoot, absolute)) : absolute;
|
|
3014
3642
|
};
|
|
3015
3643
|
var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
3016
3644
|
let raw;
|
|
3017
3645
|
try {
|
|
3018
|
-
raw =
|
|
3646
|
+
raw = readFileSync5(configPath, "utf8");
|
|
3019
3647
|
} catch {
|
|
3020
3648
|
return void 0;
|
|
3021
3649
|
}
|
|
@@ -3028,7 +3656,7 @@ var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
|
3028
3656
|
const form = evaluatedForm(mapDir, projectRoot);
|
|
3029
3657
|
if (rules.length > 0) {
|
|
3030
3658
|
if (!rules.some((rule) => compilePattern(rule).test(form))) {
|
|
3031
|
-
return `context map is not writable: none of the allow rules in ${
|
|
3659
|
+
return `context map is not writable: none of the allow rules in ${path2.basename(configPath)} match ` + (projectRoot ? `the project-root-relative form "${form}" that opencode actually evaluates for project root ${toPosix(projectRoot)}` : `the absolute form "${form}"`) + `. Rules seen: ${rules.map((r) => `"${r}"`).join(", ")}. opencode evaluates file permissions against the path RELATIVE TO THE PROJECT ROOT, so "${CONTEXT_DIR}/**" is what fires for a normally-rooted project while "*/${CONTEXT_DIR}/**" only fires when the project root is "/". Allow both forms.`;
|
|
3032
3660
|
}
|
|
3033
3661
|
}
|
|
3034
3662
|
const agents = readAgents(raw);
|
|
@@ -3061,7 +3689,7 @@ var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
|
3061
3689
|
|
|
3062
3690
|
// src/plugins/context-autoupdate.ts
|
|
3063
3691
|
var SERVICE2 = "context-autoupdate";
|
|
3064
|
-
var CONTEXT_DIR2 =
|
|
3692
|
+
var CONTEXT_DIR2 = path3.join(".opencode", "context");
|
|
3065
3693
|
var COOLDOWN_MS = 10 * 60 * 1e3;
|
|
3066
3694
|
var OWNED_TTL_MS = 20 * 60 * 1e3;
|
|
3067
3695
|
var MAX_FILES_LISTED = 25;
|
|
@@ -3098,13 +3726,19 @@ var NO_BULK_READS = [
|
|
|
3098
3726
|
"that PRODUCES or CONSUMES them instead. If a payload must be sampled, use",
|
|
3099
3727
|
"`head -c 2000` or `wc -c` \u2014 never `cat` or a bare `grep -r` across one."
|
|
3100
3728
|
].join(" ");
|
|
3101
|
-
var
|
|
3729
|
+
var ENTRY_CONTRACT = [
|
|
3730
|
+
"Every navigation entry uses the schema in your system prompt:",
|
|
3731
|
+
"`- [binding|provisional|disputed] <rule, at most three lines> \u2014 See `<path>`[ \xA7Section] (verified YYYY-MM-DD)`.",
|
|
3732
|
+
"Stay inside each file's line budget. Point at the page or path that owns a fact; do not restate it.",
|
|
3733
|
+
"After writing, run the drift pass: report a contradiction, never overwrite the page or silently pick a side."
|
|
3734
|
+
].join(" ");
|
|
3735
|
+
var toPosix2 = (value) => value.split(path3.sep).join("/");
|
|
3102
3736
|
var findProjectConfig = () => {
|
|
3103
|
-
let dir = import.meta.dir ??
|
|
3737
|
+
let dir = import.meta.dir ?? path3.dirname(new URL(import.meta.url).pathname);
|
|
3104
3738
|
for (let up = 0; up <= 3; up++) {
|
|
3105
|
-
const candidate =
|
|
3106
|
-
if (
|
|
3107
|
-
const parent =
|
|
3739
|
+
const candidate = path3.join(dir, "opencode.jsonc");
|
|
3740
|
+
if (existsSync3(candidate)) return candidate;
|
|
3741
|
+
const parent = path3.dirname(dir);
|
|
3108
3742
|
if (parent === dir) break;
|
|
3109
3743
|
dir = parent;
|
|
3110
3744
|
}
|
|
@@ -3114,9 +3748,9 @@ var snapshotMap = (dir) => {
|
|
|
3114
3748
|
const out = /* @__PURE__ */ new Map();
|
|
3115
3749
|
try {
|
|
3116
3750
|
for (const name of readdirSync2(dir)) {
|
|
3117
|
-
const file =
|
|
3751
|
+
const file = path3.join(dir, name);
|
|
3118
3752
|
try {
|
|
3119
|
-
const st =
|
|
3753
|
+
const st = statSync4(file);
|
|
3120
3754
|
if (st.isFile()) out.set(name, `${st.size}:${st.mtimeMs}`);
|
|
3121
3755
|
} catch {
|
|
3122
3756
|
}
|
|
@@ -3133,20 +3767,20 @@ var changedSince = (before, dir) => {
|
|
|
3133
3767
|
return changed;
|
|
3134
3768
|
};
|
|
3135
3769
|
var RETAIN_TRANSCRIPTS = 20;
|
|
3136
|
-
var transcriptDir = () => process.env.LACODE_TRANSCRIPT_DIR ??
|
|
3770
|
+
var transcriptDir = () => process.env.LACODE_TRANSCRIPT_DIR ?? path3.join(homedir3(), ".local", "share", "opencode", "lacode", "cartographer");
|
|
3137
3771
|
var retireSession = async (client, directory, sessionID, mode, log) => {
|
|
3138
3772
|
try {
|
|
3139
3773
|
const transcript = await client.session.messages({ path: { id: sessionID }, query: { directory } }).then((r) => r.data).catch(() => void 0);
|
|
3140
3774
|
const dir = transcriptDir();
|
|
3141
3775
|
mkdirSync3(dir, { recursive: true });
|
|
3142
3776
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3143
|
-
const file =
|
|
3144
|
-
|
|
3777
|
+
const file = path3.join(dir, `${stamp}-${mode}.json`);
|
|
3778
|
+
writeFileSync4(file, JSON.stringify({ sessionID, mode, messages: transcript ?? null }, null, 2));
|
|
3145
3779
|
pruneTranscripts(dir);
|
|
3146
3780
|
const gone = async () => await client.session.get({ path: { id: sessionID }, query: { directory } }).then((r) => r.data).catch(() => void 0) === void 0;
|
|
3147
3781
|
await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
|
|
3148
3782
|
if (await gone()) return;
|
|
3149
|
-
await new Promise((
|
|
3783
|
+
await new Promise((resolve4) => setTimeout(resolve4, 500));
|
|
3150
3784
|
await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
|
|
3151
3785
|
if (await gone()) return;
|
|
3152
3786
|
await log?.("warn", "the cartographer's session could not be removed from the session list; it stays as an empty entry", {
|
|
@@ -3162,7 +3796,7 @@ var pruneTranscripts = (dir) => {
|
|
|
3162
3796
|
const entries = readdirSync2(dir).filter((name) => name.endsWith(".json")).sort();
|
|
3163
3797
|
for (const name of entries.slice(0, Math.max(0, entries.length - RETAIN_TRANSCRIPTS))) {
|
|
3164
3798
|
try {
|
|
3165
|
-
unlinkSync3(
|
|
3799
|
+
unlinkSync3(path3.join(dir, name));
|
|
3166
3800
|
} catch {
|
|
3167
3801
|
}
|
|
3168
3802
|
}
|
|
@@ -3176,24 +3810,24 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3176
3810
|
const state2 = state(options);
|
|
3177
3811
|
const log = (level, message, extra) => client.app.log({ body: { service: SERVICE2, level, message, extra } }).catch(() => {
|
|
3178
3812
|
});
|
|
3179
|
-
const root =
|
|
3180
|
-
const contextAbs =
|
|
3181
|
-
const activeBacklogPath =
|
|
3813
|
+
const root = path3.resolve(directory);
|
|
3814
|
+
const contextAbs = path3.join(root, CONTEXT_DIR2);
|
|
3815
|
+
const activeBacklogPath = path3.join(contextAbs, ACTIVE_BACKLOG_FILE);
|
|
3182
3816
|
const config = findProjectConfig();
|
|
3183
3817
|
const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
|
|
3184
3818
|
if (writerProblem) void log("error", writerProblem);
|
|
3185
3819
|
const isTracked = (file) => {
|
|
3186
|
-
const absolute =
|
|
3187
|
-
if (absolute !== root && !absolute.startsWith(root +
|
|
3188
|
-
if (absolute === contextAbs || absolute.startsWith(contextAbs +
|
|
3820
|
+
const absolute = path3.resolve(root, file);
|
|
3821
|
+
if (absolute !== root && !absolute.startsWith(root + path3.sep)) return false;
|
|
3822
|
+
if (absolute === contextAbs || absolute.startsWith(contextAbs + path3.sep)) return false;
|
|
3189
3823
|
return !toPosix2(absolute).split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
3190
3824
|
};
|
|
3191
3825
|
const normalizeActiveBacklog = async () => {
|
|
3192
3826
|
try {
|
|
3193
|
-
if (!
|
|
3194
|
-
const before =
|
|
3827
|
+
if (!existsSync3(activeBacklogPath)) return { removed: 0 };
|
|
3828
|
+
const before = readFileSync6(activeBacklogPath, "utf8");
|
|
3195
3829
|
const { text, removed } = normalizeBacklog(before);
|
|
3196
|
-
if (text !== before)
|
|
3830
|
+
if (text !== before) writeFileSync4(activeBacklogPath, text);
|
|
3197
3831
|
if (removed.length > 0) {
|
|
3198
3832
|
await log("info", `active backlog: dropped ${removed.length} task(s) finished more than a week ago`, {
|
|
3199
3833
|
removed: removed.length
|
|
@@ -3204,7 +3838,46 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3204
3838
|
return { removed: 0 };
|
|
3205
3839
|
}
|
|
3206
3840
|
};
|
|
3207
|
-
|
|
3841
|
+
const applyLifecycle = async () => {
|
|
3842
|
+
try {
|
|
3843
|
+
const result = maintainMap(root, contextAbs);
|
|
3844
|
+
for (const file of result.files) {
|
|
3845
|
+
if (file.removed.length === 0) continue;
|
|
3846
|
+
await log(
|
|
3847
|
+
"info",
|
|
3848
|
+
`map lifecycle: dropped ${file.removed.length} expired provisional entr${file.removed.length === 1 ? "y" : "ies"} from ${file.file}`,
|
|
3849
|
+
{ file: file.file, removed: file.removed.map((entry) => entry.text) }
|
|
3850
|
+
);
|
|
3851
|
+
}
|
|
3852
|
+
return result;
|
|
3853
|
+
} catch {
|
|
3854
|
+
return void 0;
|
|
3855
|
+
}
|
|
3856
|
+
};
|
|
3857
|
+
const reportMapAudit = async (mode) => {
|
|
3858
|
+
try {
|
|
3859
|
+
const audit = auditMap(root, contextAbs);
|
|
3860
|
+
if (audit.ok) {
|
|
3861
|
+
await log("info", "context map validation passed", { mode, files: audit.files.length });
|
|
3862
|
+
return audit;
|
|
3863
|
+
}
|
|
3864
|
+
const byKind = {};
|
|
3865
|
+
for (const violation of audit.violations) {
|
|
3866
|
+
byKind[violation.kind] = (byKind[violation.kind] ?? 0) + 1;
|
|
3867
|
+
}
|
|
3868
|
+
await log("error", `context map validation failed: ${audit.violations.length} issue(s)`, {
|
|
3869
|
+
mode,
|
|
3870
|
+
byKind,
|
|
3871
|
+
issues: formatViolations(audit.violations, 20),
|
|
3872
|
+
note: "reported, not rewritten; fix the entry or its source and run /context-update"
|
|
3873
|
+
});
|
|
3874
|
+
return audit;
|
|
3875
|
+
} catch (error) {
|
|
3876
|
+
await log("warn", `context map validation could not run: ${String(error)}`, { mode });
|
|
3877
|
+
return void 0;
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3880
|
+
void applyLifecycle();
|
|
3208
3881
|
const listed = (files) => {
|
|
3209
3882
|
const shown = files.slice(0, MAX_FILES_LISTED);
|
|
3210
3883
|
const rest = files.length - shown.length;
|
|
@@ -3222,7 +3895,9 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3222
3895
|
"",
|
|
3223
3896
|
NO_BULK_READS,
|
|
3224
3897
|
"",
|
|
3225
|
-
|
|
3898
|
+
ENTRY_CONTRACT,
|
|
3899
|
+
"",
|
|
3900
|
+
"Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their line budgets. Also create `.opencode/context/active.md` with its heading and no tasks. When the project already has architecture or decision documentation, link to it instead of duplicating it. Write conclusions, not inventories."
|
|
3226
3901
|
].join("\n") : [
|
|
3227
3902
|
"Incremental context update. Project root: " + directory,
|
|
3228
3903
|
"",
|
|
@@ -3231,6 +3906,8 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3231
3906
|
"",
|
|
3232
3907
|
NO_BULK_READS,
|
|
3233
3908
|
"",
|
|
3909
|
+
ENTRY_CONTRACT,
|
|
3910
|
+
"",
|
|
3234
3911
|
'Follow the incremental-update procedure in your system prompt. Update a context file only if this work introduced or revealed information that will materially help future agents: a new boundary or module, a new or moved canonical entry point, a changed workflow, a new convention, an architectural decision, a new dependency between subsystems, a renamed or moved subsystem, or an invariant not yet recorded. Do not edit documentation merely because source files changed. If nothing material changed, make no edits and report "no update needed". Also keep `.opencode/context/active.md` current: record work that is in flight, and mark a task finished \u2014 with the date it finished \u2014 when it is.'
|
|
3235
3912
|
].join("\n");
|
|
3236
3913
|
return {
|
|
@@ -3238,7 +3915,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3238
3915
|
try {
|
|
3239
3916
|
if (event.type === "file.edited") {
|
|
3240
3917
|
const file = event.properties.file;
|
|
3241
|
-
if (
|
|
3918
|
+
if (path3.resolve(root, file) === activeBacklogPath) {
|
|
3242
3919
|
await normalizeActiveBacklog();
|
|
3243
3920
|
return;
|
|
3244
3921
|
}
|
|
@@ -3265,7 +3942,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3265
3942
|
const requeue = () => {
|
|
3266
3943
|
for (const file of files) pending.add(file);
|
|
3267
3944
|
};
|
|
3268
|
-
const mode =
|
|
3945
|
+
const mode = existsSync3(path3.join(contextAbs, "contexts.md")) ? "update" : "bootstrap";
|
|
3269
3946
|
init(state2, (m) => void log("warn", m));
|
|
3270
3947
|
const snap = snapshot(state2);
|
|
3271
3948
|
const slot = pickSlot(state2, snap);
|
|
@@ -3332,6 +4009,13 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3332
4009
|
hint: "either nothing needed updating, or the cartographer could not write -- check the one-writer permission rule"
|
|
3333
4010
|
});
|
|
3334
4011
|
}
|
|
4012
|
+
await applyLifecycle();
|
|
4013
|
+
const audit = await reportMapAudit(mode);
|
|
4014
|
+
if (audit && !audit.ok) {
|
|
4015
|
+
await log("error", `auto ${mode} finished with ${audit.violations.length} map validation issue(s)`, {
|
|
4016
|
+
sessionID: target
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
3335
4019
|
}).catch(async (error) => {
|
|
3336
4020
|
release(state2, claimKey2);
|
|
3337
4021
|
await retireSession(client, directory, target, mode, log);
|
|
@@ -3345,11 +4029,92 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
3345
4029
|
};
|
|
3346
4030
|
});
|
|
3347
4031
|
|
|
4032
|
+
// src/lib/feedback.ts
|
|
4033
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4034
|
+
import { homedir as homedir4 } from "node:os";
|
|
4035
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
4036
|
+
var FEEDBACK_HINT_ENV = "LACODE_NO_FEEDBACK_HINT";
|
|
4037
|
+
var FEEDBACK_HINT = "Enjoying lacode? Bugs, ideas, or a quick note on how you use it: github.com/lawaty/lacode/issues (a \u2B50 on the repo helps others find it)";
|
|
4038
|
+
var feedbackStatePath = () => join7(homedir4(), ".local", "share", "opencode", "lacode", "feedback.json");
|
|
4039
|
+
function readFeedbackState(path5 = feedbackStatePath()) {
|
|
4040
|
+
try {
|
|
4041
|
+
const parsed = JSON.parse(readFileSync7(path5, "utf8"));
|
|
4042
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
4043
|
+
return parsed;
|
|
4044
|
+
} catch (error) {
|
|
4045
|
+
if (error?.code === "ENOENT") return {};
|
|
4046
|
+
return void 0;
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
function markFeedbackHintShown(path5 = feedbackStatePath()) {
|
|
4050
|
+
try {
|
|
4051
|
+
mkdirSync4(dirname4(path5), { recursive: true });
|
|
4052
|
+
const tmp = `${path5}.${process.pid}.tmp`;
|
|
4053
|
+
writeFileSync5(tmp, JSON.stringify({ feedbackHintShown: true }));
|
|
4054
|
+
renameSync3(tmp, path5);
|
|
4055
|
+
return true;
|
|
4056
|
+
} catch {
|
|
4057
|
+
return false;
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
function restoreFeedbackState(path5 = feedbackStatePath(), previous = {}) {
|
|
4061
|
+
try {
|
|
4062
|
+
if (Object.keys(previous).length === 0) {
|
|
4063
|
+
unlinkSync4(path5);
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
const tmp = `${path5}.${process.pid}.tmp`;
|
|
4067
|
+
writeFileSync5(tmp, JSON.stringify(previous));
|
|
4068
|
+
renameSync3(tmp, path5);
|
|
4069
|
+
} catch {
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
async function maybeShowFeedbackHint(deps = {}) {
|
|
4073
|
+
const env = deps.env ?? process.env;
|
|
4074
|
+
if (deps.enabled === false || deps.feedbackHint === false) return "disabled";
|
|
4075
|
+
if (env[FEEDBACK_HINT_ENV] === "1") return "disabled";
|
|
4076
|
+
const interactive = deps.interactive ?? process.stdout.isTTY === true;
|
|
4077
|
+
if (!interactive || env.CI || env.NODE_TEST_CONTEXT) return "unavailable";
|
|
4078
|
+
if (!deps.show) return "unavailable";
|
|
4079
|
+
const state2 = readFeedbackState(deps.statePath);
|
|
4080
|
+
if (state2 === void 0 || state2.feedbackHintShown === true) return "skipped";
|
|
4081
|
+
if (!markFeedbackHintShown(deps.statePath)) return "skipped";
|
|
4082
|
+
try {
|
|
4083
|
+
await deps.show(FEEDBACK_HINT);
|
|
4084
|
+
} catch {
|
|
4085
|
+
restoreFeedbackState(deps.statePath, state2);
|
|
4086
|
+
return "skipped";
|
|
4087
|
+
}
|
|
4088
|
+
return "shown";
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
// src/plugins/feedback.ts
|
|
4092
|
+
async function feedbackHooks(input, options, deps = {}) {
|
|
4093
|
+
const opts = options ?? {};
|
|
4094
|
+
if (opts.enabled === false || opts.feedbackHint === false) return {};
|
|
4095
|
+
const tui = input.client.tui;
|
|
4096
|
+
const show = deps.show ?? (tui?.showToast ? async (message) => {
|
|
4097
|
+
await tui.showToast({
|
|
4098
|
+
body: { title: "lacode", message, variant: "info", duration: 8e3 }
|
|
4099
|
+
});
|
|
4100
|
+
} : void 0);
|
|
4101
|
+
void maybeShowFeedbackHint({
|
|
4102
|
+
enabled: opts.enabled,
|
|
4103
|
+
feedbackHint: opts.feedbackHint,
|
|
4104
|
+
env: deps.env,
|
|
4105
|
+
statePath: deps.statePath,
|
|
4106
|
+
interactive: deps.interactive,
|
|
4107
|
+
show
|
|
4108
|
+
}).catch(() => {
|
|
4109
|
+
});
|
|
4110
|
+
return {};
|
|
4111
|
+
}
|
|
4112
|
+
|
|
3348
4113
|
// src/plugins/image-paste.ts
|
|
3349
4114
|
import { createHash, randomUUID } from "node:crypto";
|
|
3350
|
-
import { globSync, mkdirSync as
|
|
3351
|
-
import { homedir as
|
|
3352
|
-
import { basename as basename2, join as
|
|
4115
|
+
import { globSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4116
|
+
import { homedir as homedir5 } from "node:os";
|
|
4117
|
+
import { basename as basename2, join as join8 } from "node:path";
|
|
3353
4118
|
|
|
3354
4119
|
// src/lib/clipboard.ts
|
|
3355
4120
|
import { spawnSync } from "node:child_process";
|
|
@@ -3410,12 +4175,12 @@ var WINDOWS_SCRIPT = [
|
|
|
3410
4175
|
var readWindows = async (run) => {
|
|
3411
4176
|
const result = await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_SCRIPT]);
|
|
3412
4177
|
if (!result.ok) return null;
|
|
3413
|
-
const
|
|
3414
|
-
if (!
|
|
4178
|
+
const path5 = result.stdout.toString("utf8").replace(/^\uFEFF/, "").trim();
|
|
4179
|
+
if (!path5) return null;
|
|
3415
4180
|
try {
|
|
3416
|
-
return validImage({ ok: true, stdout: await readFile(
|
|
4181
|
+
return validImage({ ok: true, stdout: await readFile(path5) });
|
|
3417
4182
|
} finally {
|
|
3418
|
-
await rm(
|
|
4183
|
+
await rm(path5, { force: true }).catch(() => {
|
|
3419
4184
|
});
|
|
3420
4185
|
}
|
|
3421
4186
|
};
|
|
@@ -3499,8 +4264,8 @@ function splitArguments(input) {
|
|
|
3499
4264
|
return out;
|
|
3500
4265
|
}
|
|
3501
4266
|
function expandTilde(p) {
|
|
3502
|
-
if (p === "~") return
|
|
3503
|
-
if (p.startsWith("~/")) return
|
|
4267
|
+
if (p === "~") return homedir5();
|
|
4268
|
+
if (p.startsWith("~/")) return join8(homedir5(), p.slice(2));
|
|
3504
4269
|
return p;
|
|
3505
4270
|
}
|
|
3506
4271
|
function expandGlob(p) {
|
|
@@ -3515,11 +4280,11 @@ function storeImage(bytes, dir, sessionID, label) {
|
|
|
3515
4280
|
const kind = detectImageKind(bytes);
|
|
3516
4281
|
if (kind === null) return null;
|
|
3517
4282
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
3518
|
-
|
|
3519
|
-
const target =
|
|
4283
|
+
mkdirSync5(dir, { recursive: true });
|
|
4284
|
+
const target = join8(dir, `${hash}.${kind.ext}`);
|
|
3520
4285
|
const tmp = `${target}.tmp`;
|
|
3521
|
-
|
|
3522
|
-
|
|
4286
|
+
writeFileSync6(tmp, bytes, { mode: 384 });
|
|
4287
|
+
renameSync4(tmp, target);
|
|
3523
4288
|
const part = {
|
|
3524
4289
|
id: randomUUID(),
|
|
3525
4290
|
sessionID,
|
|
@@ -3547,7 +4312,7 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
3547
4312
|
if (opts.enabled === false) return {};
|
|
3548
4313
|
const maxBytes = resolveMaxBytes(opts);
|
|
3549
4314
|
const maxImages = resolveMaxImages(opts);
|
|
3550
|
-
const dir = opts.dir ?? process.env.LACODE_IMAGE_DIR ??
|
|
4315
|
+
const dir = opts.dir ?? process.env.LACODE_IMAGE_DIR ?? join8(homedir5(), ".local/share/opencode/lacode/images");
|
|
3551
4316
|
const readClipboard = (deps ?? {}).readClipboard ?? readClipboardImage;
|
|
3552
4317
|
return {
|
|
3553
4318
|
"command.execute.before": async (cmd, output) => {
|
|
@@ -3588,7 +4353,7 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
3588
4353
|
const name = basename2(filePath);
|
|
3589
4354
|
let size;
|
|
3590
4355
|
try {
|
|
3591
|
-
const stat =
|
|
4356
|
+
const stat = statSync5(filePath);
|
|
3592
4357
|
if (!stat.isFile()) {
|
|
3593
4358
|
skipped.push(name);
|
|
3594
4359
|
continue;
|
|
@@ -3604,7 +4369,7 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
3604
4369
|
}
|
|
3605
4370
|
let bytes;
|
|
3606
4371
|
try {
|
|
3607
|
-
bytes =
|
|
4372
|
+
bytes = readFileSync8(filePath);
|
|
3608
4373
|
} catch {
|
|
3609
4374
|
skipped.push(name);
|
|
3610
4375
|
continue;
|
|
@@ -3663,13 +4428,13 @@ function scrubSlotNames(text) {
|
|
|
3663
4428
|
// src/plugins/notify.ts
|
|
3664
4429
|
import { spawn as spawn2 } from "node:child_process";
|
|
3665
4430
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3666
|
-
import { existsSync as
|
|
3667
|
-
import * as
|
|
4431
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4432
|
+
import * as path4 from "node:path";
|
|
3668
4433
|
|
|
3669
4434
|
// src/lib/notify.ts
|
|
3670
|
-
import { closeSync as closeSync2, mkdirSync as
|
|
3671
|
-
import { homedir as
|
|
3672
|
-
import { basename as basename3, join as
|
|
4435
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync9, renameSync as renameSync5, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7, writeSync as writeSync2 } from "node:fs";
|
|
4436
|
+
import { homedir as homedir6 } from "node:os";
|
|
4437
|
+
import { basename as basename3, join as join9 } from "node:path";
|
|
3673
4438
|
var INTERNAL_TITLE_PREFIXES = [
|
|
3674
4439
|
// context-autoupdate spawns the cartographer through the session API, with no
|
|
3675
4440
|
// parentID, so it looks exactly like a root session going idle. Notifying for
|
|
@@ -3846,7 +4611,7 @@ var Notifier = class {
|
|
|
3846
4611
|
return this.lastSent.size;
|
|
3847
4612
|
}
|
|
3848
4613
|
};
|
|
3849
|
-
var notificationStateDir = () => process.env.LACODE_NOTIFY_DIR ??
|
|
4614
|
+
var notificationStateDir = () => process.env.LACODE_NOTIFY_DIR ?? join9(homedir6(), ".local", "share", "opencode", "lacode", "notify");
|
|
3850
4615
|
var safeCorrelationKey = (correlationKey) => correlationKey.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
3851
4616
|
var CLAIM_WINDOW_MS = 5e3;
|
|
3852
4617
|
var isPidAlive2 = (pid) => {
|
|
@@ -3857,17 +4622,17 @@ var isPidAlive2 = (pid) => {
|
|
|
3857
4622
|
return error.code === "EPERM";
|
|
3858
4623
|
}
|
|
3859
4624
|
};
|
|
3860
|
-
var claimFile = (sessionID, kind) =>
|
|
4625
|
+
var claimFile = (sessionID, kind) => join9(notificationStateDir(), `claim.${safeCorrelationKey(`${sessionID}.${kind}`)}`);
|
|
3861
4626
|
var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
3862
|
-
const
|
|
4627
|
+
const path5 = claimFile(sessionID, kind);
|
|
3863
4628
|
try {
|
|
3864
|
-
|
|
4629
|
+
mkdirSync6(notificationStateDir(), { recursive: true });
|
|
3865
4630
|
} catch {
|
|
3866
4631
|
return true;
|
|
3867
4632
|
}
|
|
3868
4633
|
const take = () => {
|
|
3869
4634
|
try {
|
|
3870
|
-
const fd = openSync2(
|
|
4635
|
+
const fd = openSync2(path5, "wx");
|
|
3871
4636
|
try {
|
|
3872
4637
|
writeSync2(fd, `${process.pid} ${instanceId} ${kind}`);
|
|
3873
4638
|
} finally {
|
|
@@ -3885,7 +4650,7 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3885
4650
|
const judge = () => {
|
|
3886
4651
|
let raw;
|
|
3887
4652
|
try {
|
|
3888
|
-
raw =
|
|
4653
|
+
raw = readFileSync9(path5, "utf8").trim();
|
|
3889
4654
|
} catch (error) {
|
|
3890
4655
|
return error.code === "ENOENT" ? "gone" : "unknown";
|
|
3891
4656
|
}
|
|
@@ -3906,7 +4671,7 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3906
4671
|
}
|
|
3907
4672
|
let age = Number.POSITIVE_INFINITY;
|
|
3908
4673
|
try {
|
|
3909
|
-
age = Date.now() -
|
|
4674
|
+
age = Date.now() - statSync6(path5).mtimeMs;
|
|
3910
4675
|
} catch {
|
|
3911
4676
|
return "gone";
|
|
3912
4677
|
}
|
|
@@ -3923,7 +4688,7 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3923
4688
|
if (judged === "held") return false;
|
|
3924
4689
|
if (judged === "unknown") return true;
|
|
3925
4690
|
if (judged === "gone") return take() !== "held";
|
|
3926
|
-
const lockPath = `${
|
|
4691
|
+
const lockPath = `${path5}.takeover`;
|
|
3927
4692
|
const takeOver = () => {
|
|
3928
4693
|
const create = () => {
|
|
3929
4694
|
try {
|
|
@@ -3935,7 +4700,7 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3935
4700
|
}
|
|
3936
4701
|
return () => {
|
|
3937
4702
|
try {
|
|
3938
|
-
|
|
4703
|
+
unlinkSync5(lockPath);
|
|
3939
4704
|
} catch {
|
|
3940
4705
|
}
|
|
3941
4706
|
};
|
|
@@ -3949,9 +4714,9 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3949
4714
|
let lockHolder;
|
|
3950
4715
|
let lockAge = Number.POSITIVE_INFINITY;
|
|
3951
4716
|
try {
|
|
3952
|
-
const parsed = Number.parseInt(
|
|
4717
|
+
const parsed = Number.parseInt(readFileSync9(lockPath, "utf8").trim(), 10);
|
|
3953
4718
|
lockHolder = Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
3954
|
-
lockAge = Date.now() -
|
|
4719
|
+
lockAge = Date.now() - statSync6(lockPath).mtimeMs;
|
|
3955
4720
|
} catch {
|
|
3956
4721
|
return create();
|
|
3957
4722
|
}
|
|
@@ -3959,7 +4724,7 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3959
4724
|
return void 0;
|
|
3960
4725
|
}
|
|
3961
4726
|
try {
|
|
3962
|
-
|
|
4727
|
+
unlinkSync5(lockPath);
|
|
3963
4728
|
} catch {
|
|
3964
4729
|
return void 0;
|
|
3965
4730
|
}
|
|
@@ -3973,7 +4738,7 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3973
4738
|
if (current === "held") return false;
|
|
3974
4739
|
if (current === "unknown") return true;
|
|
3975
4740
|
try {
|
|
3976
|
-
|
|
4741
|
+
unlinkSync5(path5);
|
|
3977
4742
|
} catch {
|
|
3978
4743
|
}
|
|
3979
4744
|
return take() !== "held";
|
|
@@ -3982,23 +4747,23 @@ var claimNotification = (sessionID, kind, instanceId = "") => {
|
|
|
3982
4747
|
}
|
|
3983
4748
|
};
|
|
3984
4749
|
var releaseNotificationClaim = (sessionID, kind) => {
|
|
3985
|
-
const
|
|
4750
|
+
const path5 = claimFile(sessionID, kind);
|
|
3986
4751
|
try {
|
|
3987
|
-
const parsed = Number.parseInt(
|
|
4752
|
+
const parsed = Number.parseInt(readFileSync9(path5, "utf8").trim(), 10);
|
|
3988
4753
|
if (!Number.isFinite(parsed) || parsed <= 0) return;
|
|
3989
4754
|
if (parsed !== process.pid) return;
|
|
3990
|
-
|
|
4755
|
+
unlinkSync5(path5);
|
|
3991
4756
|
} catch {
|
|
3992
4757
|
}
|
|
3993
4758
|
};
|
|
3994
|
-
var completionFile = (sessionID) =>
|
|
4759
|
+
var completionFile = (sessionID) => join9(notificationStateDir(), `completion.${safeCorrelationKey(sessionID)}`);
|
|
3995
4760
|
var claimCompletion = (sessionID, kind) => {
|
|
3996
|
-
const
|
|
4761
|
+
const path5 = completionFile(sessionID);
|
|
3997
4762
|
try {
|
|
3998
|
-
|
|
3999
|
-
const fd = openSync2(
|
|
4763
|
+
mkdirSync6(notificationStateDir(), { recursive: true });
|
|
4764
|
+
const fd = openSync2(path5, "wx");
|
|
4000
4765
|
try {
|
|
4001
|
-
|
|
4766
|
+
writeFileSync7(fd, `${process.pid} ${kind}
|
|
4002
4767
|
`);
|
|
4003
4768
|
} finally {
|
|
4004
4769
|
closeSync2(fd);
|
|
@@ -4011,18 +4776,18 @@ var claimCompletion = (sessionID, kind) => {
|
|
|
4011
4776
|
};
|
|
4012
4777
|
var releaseCompletion = (sessionID) => {
|
|
4013
4778
|
try {
|
|
4014
|
-
|
|
4779
|
+
unlinkSync5(completionFile(sessionID));
|
|
4015
4780
|
} catch {
|
|
4016
4781
|
}
|
|
4017
4782
|
};
|
|
4018
4783
|
var markCompletion = (sessionID, kind) => {
|
|
4019
4784
|
try {
|
|
4020
|
-
|
|
4021
|
-
const
|
|
4022
|
-
const tmp = `${
|
|
4023
|
-
|
|
4785
|
+
mkdirSync6(notificationStateDir(), { recursive: true });
|
|
4786
|
+
const path5 = completionFile(sessionID);
|
|
4787
|
+
const tmp = `${path5}.${process.pid}.tmp`;
|
|
4788
|
+
writeFileSync7(tmp, `${process.pid} ${kind}
|
|
4024
4789
|
`);
|
|
4025
|
-
|
|
4790
|
+
renameSync5(tmp, path5);
|
|
4026
4791
|
} catch {
|
|
4027
4792
|
}
|
|
4028
4793
|
};
|
|
@@ -4043,13 +4808,13 @@ var findHelper = () => {
|
|
|
4043
4808
|
const home = process.env.HOME ?? "";
|
|
4044
4809
|
const candidates = [
|
|
4045
4810
|
process.env.OC_NOTIFY_HELPER,
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4811
|
+
path4.join(home, "bin", "lacode"),
|
|
4812
|
+
path4.join(PACKAGE_ROOT, "dist", "lacode.js"),
|
|
4813
|
+
path4.join(PACKAGE_ROOT, "bin", "lacode"),
|
|
4814
|
+
path4.join(home, "bin", "oc-notify"),
|
|
4815
|
+
path4.join(PACKAGE_ROOT, "bin", "oc-notify")
|
|
4051
4816
|
].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
4052
|
-
for (const candidate of candidates) if (
|
|
4817
|
+
for (const candidate of candidates) if (existsSync4(candidate)) return candidate;
|
|
4053
4818
|
return void 0;
|
|
4054
4819
|
};
|
|
4055
4820
|
var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
@@ -4162,10 +4927,10 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4162
4927
|
const info = await client.session.get({ path: { id: sessionID }, query: { directory } }).then((result) => result.data).catch(() => void 0);
|
|
4163
4928
|
return info;
|
|
4164
4929
|
};
|
|
4165
|
-
const deliverViaHelper = (title, message, urgency, sound, requestID, sessionID) => new Promise((
|
|
4930
|
+
const deliverViaHelper = (title, message, urgency, sound, requestID, sessionID) => new Promise((resolve4) => {
|
|
4166
4931
|
if (inflight >= maxInflight) {
|
|
4167
4932
|
void log("warn", `notification dropped: ${inflight} helpers already in flight`, { title });
|
|
4168
|
-
|
|
4933
|
+
resolve4({ ok: false, detail: `too many notification helpers in flight (${inflight})` });
|
|
4169
4934
|
return;
|
|
4170
4935
|
}
|
|
4171
4936
|
const helper = findHelper();
|
|
@@ -4174,7 +4939,7 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4174
4939
|
warnedHelper = true;
|
|
4175
4940
|
void log("warn", "no oc-notify helper found; run 'bin/oc-sync' to install one");
|
|
4176
4941
|
}
|
|
4177
|
-
|
|
4942
|
+
resolve4({ ok: false, detail: "no oc-notify helper found" });
|
|
4178
4943
|
return;
|
|
4179
4944
|
}
|
|
4180
4945
|
const childEnv = { ...process.env, ...sessionID ? { [NOTIFY_SESSION_ENV]: sessionID } : {} };
|
|
@@ -4197,7 +4962,7 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4197
4962
|
settled = true;
|
|
4198
4963
|
inflight--;
|
|
4199
4964
|
if (timer) clearTimeout(timer);
|
|
4200
|
-
|
|
4965
|
+
resolve4(result);
|
|
4201
4966
|
};
|
|
4202
4967
|
timer = setTimeout(() => {
|
|
4203
4968
|
try {
|
|
@@ -4518,9 +5283,9 @@ function normalizeBaseURL(raw) {
|
|
|
4518
5283
|
return null;
|
|
4519
5284
|
}
|
|
4520
5285
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
4521
|
-
let
|
|
4522
|
-
while (
|
|
4523
|
-
return `${url.origin}${
|
|
5286
|
+
let path5 = url.pathname.replace(/\/+$/, "");
|
|
5287
|
+
while (path5.endsWith("/v1")) path5 = path5.slice(0, -"/v1".length);
|
|
5288
|
+
return `${url.origin}${path5}/v1`;
|
|
4524
5289
|
}
|
|
4525
5290
|
function parseConfig(raw, env = process.env) {
|
|
4526
5291
|
if (typeof raw !== "object" || raw === null) return { enabled: false, config: null, problems: [] };
|
|
@@ -5026,6 +5791,7 @@ var LaCode = (async (input, options) => {
|
|
|
5026
5791
|
const autoupdate = await settled("context-autoupdate", () => contextAutoUpdateHooks(input, options)) ?? inertHooks;
|
|
5027
5792
|
const notifier = await settled("notify", () => notifyHooks(input, options)) ?? inertHooks;
|
|
5028
5793
|
const selfupdate = await settled("self-update", () => selfUpdateHooks(input, options)) ?? inertHooks;
|
|
5794
|
+
const feedback = await settled("feedback", () => feedbackHooks(input, options)) ?? inertHooks;
|
|
5029
5795
|
const omniroute = await settled("omniroute", () => omniRouteHooks(input, options)) ?? inertHooks;
|
|
5030
5796
|
const imagepaste = await settled("image-paste", () => imagePasteHooks(input, opts.imagePaste)) ?? inertHooks;
|
|
5031
5797
|
const config = async (cfg) => {
|
|
@@ -5068,6 +5834,7 @@ var LaCode = (async (input, options) => {
|
|
|
5068
5834
|
await autoupdate.dispose?.();
|
|
5069
5835
|
await notifier.dispose?.();
|
|
5070
5836
|
await selfupdate.dispose?.();
|
|
5837
|
+
await feedback.dispose?.();
|
|
5071
5838
|
await omniroute.dispose?.();
|
|
5072
5839
|
await imagepaste.dispose?.();
|
|
5073
5840
|
},
|