hippocamp 0.2.8 → 0.2.10
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.
|
@@ -106,14 +106,14 @@ jobs:
|
|
|
106
106
|
--target-chars "$HIPPOCAMP_DREAM_TARGET_CHARS" \
|
|
107
107
|
> "/tmp/hippocamp-dream-${project}.json"
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
git add -- "projects/${project}/current_state.md" "projects/${project}/open_threads.md"
|
|
110
|
+
if git diff --cached --quiet; then
|
|
110
111
|
echo "No Dream diff for ${project}"
|
|
111
112
|
git checkout main
|
|
112
113
|
git reset --hard origin/main
|
|
113
114
|
continue
|
|
114
115
|
fi
|
|
115
116
|
|
|
116
|
-
git add "projects/${project}/current_state.md" "projects/${project}/open_threads.md"
|
|
117
117
|
git commit -m "Dream update ${project} memory"
|
|
118
118
|
git push --force-with-lease origin "$branch"
|
|
119
119
|
|
|
@@ -113,12 +113,12 @@ while IFS= read -r project; do
|
|
|
113
113
|
continue
|
|
114
114
|
fi
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
git add -- "projects/${project}/current_state.md" "projects/${project}/open_threads.md"
|
|
117
|
+
if git diff --cached --quiet; then
|
|
117
118
|
echo "No Dream diff for ${project}"
|
|
118
119
|
continue
|
|
119
120
|
fi
|
|
120
121
|
|
|
121
|
-
git add "projects/${project}/current_state.md" "projects/${project}/open_threads.md"
|
|
122
122
|
git commit -m "Dream update ${project} memory"
|
|
123
123
|
|
|
124
124
|
if git ls-remote --exit-code --heads origin "refs/heads/${branch}" >/dev/null 2>&1; then
|
package/docs/dream.md
CHANGED
|
@@ -17,6 +17,8 @@ Recent events use at most half of the evidence budget. The remaining budget supp
|
|
|
17
17
|
|
|
18
18
|
Dream promotes clear decisions, explicit corrections, and reusable project lessons into `current_state.md`, with source references. It keeps remaining actions in `open_threads.md`, preserves unresolved blockers, and removes completed actions only when evidence supports it. A merged PR does not prove its deployment or follow-up work is done. Uncertain lessons remain in events for normal search.
|
|
19
19
|
|
|
20
|
+
Projects with indexed events can start without curated summaries. Dream creates the two files, and the scheduled runners include these new files in the project's PR.
|
|
21
|
+
|
|
20
22
|
Corrections apply across both files: a shipped feature should not remain an implementation task, while unverified deployment stays open. Extra compaction passes must keep source paths and timestamps for retained knowledge and aim below the size limit to leave room for length estimates. Dream writes the review comment itself; the model does not set review progress.
|
|
21
23
|
|
|
22
24
|
The snapshot may grow within `--target-chars` when new evidence adds useful knowledge. If it exceeds the target, Dream compacts the complete result again. It never crops a wake-up file. After three passes, it fails without writing either file or advancing the event comment if the snapshot still does not fit. A project below the threshold with no new indexed evidence is skipped without a model call.
|
package/package.json
CHANGED
|
@@ -510,12 +510,19 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
if (!diffResult.stdout) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
513
|
+
// Commits left behind by an earlier failed push still need to go up.
|
|
514
|
+
const aheadResult = await runGit(["rev-list", "--count", "@{u}..HEAD"], repoRoot);
|
|
515
|
+
|
|
516
|
+
if (!aheadResult.ok || aheadResult.stdout === "0") {
|
|
517
|
+
return {
|
|
518
|
+
attempted: true,
|
|
519
|
+
skipped: true,
|
|
520
|
+
reason: "no_changes_to_commit",
|
|
521
|
+
repoRoot,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return pushToUpstream(repoRoot, { attempted: true, committed: false, repoRoot });
|
|
519
526
|
}
|
|
520
527
|
|
|
521
528
|
const commitResult = await runGit(["commit", "-m", message, "--", ...repoRelativePaths], repoRoot);
|
|
@@ -524,16 +531,22 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
|
|
|
524
531
|
throw new Error(commitResult.stderr || "git commit failed.");
|
|
525
532
|
}
|
|
526
533
|
|
|
534
|
+
return pushToUpstream(repoRoot, {
|
|
535
|
+
attempted: true,
|
|
536
|
+
committed: true,
|
|
537
|
+
repoRoot,
|
|
538
|
+
commitSummary: commitResult.stdout,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function pushToUpstream(repoRoot, result) {
|
|
527
543
|
const upstreamResult = await runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot);
|
|
528
544
|
|
|
529
545
|
if (!upstreamResult.ok) {
|
|
530
546
|
return {
|
|
531
|
-
|
|
532
|
-
committed: true,
|
|
547
|
+
...result,
|
|
533
548
|
pushed: false,
|
|
534
549
|
reason: "no_upstream",
|
|
535
|
-
repoRoot,
|
|
536
|
-
commitSummary: commitResult.stdout,
|
|
537
550
|
};
|
|
538
551
|
}
|
|
539
552
|
|
|
@@ -544,12 +557,9 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
|
|
|
544
557
|
|
|
545
558
|
if (!pullResult.ok) {
|
|
546
559
|
return {
|
|
547
|
-
|
|
548
|
-
committed: true,
|
|
560
|
+
...result,
|
|
549
561
|
pushed: false,
|
|
550
562
|
reason: "pull_rebase_failed",
|
|
551
|
-
repoRoot,
|
|
552
|
-
commitSummary: commitResult.stdout,
|
|
553
563
|
pushError: pushResult.stderr,
|
|
554
564
|
pullError: pullResult.stderr,
|
|
555
565
|
};
|
|
@@ -559,23 +569,17 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
|
|
|
559
569
|
|
|
560
570
|
if (!pushResult.ok) {
|
|
561
571
|
return {
|
|
562
|
-
|
|
563
|
-
committed: true,
|
|
572
|
+
...result,
|
|
564
573
|
pushed: false,
|
|
565
574
|
reason: "push_failed",
|
|
566
|
-
repoRoot,
|
|
567
|
-
commitSummary: commitResult.stdout,
|
|
568
575
|
pushError: pushResult.stderr,
|
|
569
576
|
};
|
|
570
577
|
}
|
|
571
578
|
}
|
|
572
579
|
|
|
573
580
|
return {
|
|
574
|
-
|
|
575
|
-
committed: true,
|
|
581
|
+
...result,
|
|
576
582
|
pushed: true,
|
|
577
|
-
repoRoot,
|
|
578
|
-
commitSummary: commitResult.stdout,
|
|
579
583
|
pushSummary: pushResult.stdout || pushResult.stderr,
|
|
580
584
|
};
|
|
581
585
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const { execFile } = require("node:child_process");
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { promisify } = require("node:util");
|
|
7
|
+
const test = require("node:test");
|
|
8
|
+
const memory = require("../scripts/hippocamp-memory.cjs");
|
|
9
|
+
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
test("Dream runner publishes first summaries and skips an unchanged repeat", async (t) => {
|
|
13
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-runner-test-"));
|
|
14
|
+
const seed = path.join(root, "seed");
|
|
15
|
+
const remote = path.join(root, "remote.git");
|
|
16
|
+
const bin = path.join(root, "bin");
|
|
17
|
+
const calls = path.join(root, "prs");
|
|
18
|
+
const preload = path.join(root, "model.cjs");
|
|
19
|
+
const previousRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
20
|
+
t.after(async () => {
|
|
21
|
+
if (previousRoot === undefined) delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
22
|
+
else process.env.HIPPOCAMP_GLOBAL_ROOT = previousRoot;
|
|
23
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
await fs.mkdir(seed);
|
|
26
|
+
await fs.mkdir(bin);
|
|
27
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = seed;
|
|
28
|
+
await memory.appendEvent({ projectRoot: "/tmp/new-project", scope: "project", sync: false, title: "Confirmed decision", keywords: ["constraint"], content: "Keep production access read-only." });
|
|
29
|
+
const env = {
|
|
30
|
+
...process.env,
|
|
31
|
+
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
|
32
|
+
GIT_CONFIG_GLOBAL: path.join(root, "gitconfig"),
|
|
33
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
34
|
+
GITHUB_TOKEN: "fixture",
|
|
35
|
+
LAGOON_REPOSITORY: "fixture/lagoon",
|
|
36
|
+
MANIFEST_API_KEY: "fixture",
|
|
37
|
+
MANIFEST_BASE_URL: "https://model.invalid",
|
|
38
|
+
HIPPOCAMP_DREAM_AUTO_MERGE: "false",
|
|
39
|
+
NODE_OPTIONS: `--require=${preload}`,
|
|
40
|
+
FIXTURE_REMOTE: remote,
|
|
41
|
+
FIXTURE_CALLS: calls,
|
|
42
|
+
};
|
|
43
|
+
await fs.writeFile(env.GIT_CONFIG_GLOBAL, "[user]\nname = Fixture\nemail = fixture@localhost\n");
|
|
44
|
+
await fs.writeFile(preload, `
|
|
45
|
+
global.fetch = async () => {
|
|
46
|
+
const fs = require("node:fs");
|
|
47
|
+
const path = require("node:path");
|
|
48
|
+
fs.writeFileSync(path.join(process.env.HIPPOCAMP_GLOBAL_ROOT, "unrelated.md"), "Do not publish this file");
|
|
49
|
+
return { ok: true, status: 200, text: async () => JSON.stringify({ output_text: JSON.stringify({
|
|
50
|
+
current_state_md: "# Current State\\n\\n- Keep production access read-only.",
|
|
51
|
+
open_threads_md: "# Open Threads\\n\\n- Verify deployment."
|
|
52
|
+
}) }) };
|
|
53
|
+
};
|
|
54
|
+
`);
|
|
55
|
+
await fs.writeFile(path.join(bin, "gh"), `#!${process.execPath}
|
|
56
|
+
const fs = require("node:fs");
|
|
57
|
+
const { execFileSync } = require("node:child_process");
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
if (args[0] === "auth") process.exit(0);
|
|
60
|
+
if (args[0] === "repo" && args[1] === "clone") {
|
|
61
|
+
execFileSync("git", ["clone", process.env.FIXTURE_REMOTE, args[3]], { stdio: "inherit" });
|
|
62
|
+
} else if (args[0] === "pr" && args[1] === "list") {
|
|
63
|
+
if (fs.existsSync(process.env.FIXTURE_CALLS)) console.log("1");
|
|
64
|
+
} else if (args[0] === "pr" && args[1] === "create") {
|
|
65
|
+
fs.appendFileSync(process.env.FIXTURE_CALLS, "created\\n");
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error("Unexpected gh operation: " + args.join(" "));
|
|
68
|
+
}
|
|
69
|
+
`, { mode: 0o755 });
|
|
70
|
+
const git = (args, cwd = seed) => exec("git", args, { cwd, env });
|
|
71
|
+
await git(["init", "-b", "main"]);
|
|
72
|
+
await git(["add", "."]);
|
|
73
|
+
await git(["commit", "-m", "Seed indexed evidence without curated files"]);
|
|
74
|
+
const before = (await git(["rev-parse", "HEAD"])).stdout.trim();
|
|
75
|
+
await git(["clone", "--bare", seed, remote]);
|
|
76
|
+
const runner = path.resolve(__dirname, "../assets/railway/run-dream.sh");
|
|
77
|
+
await exec("bash", [runner], { env });
|
|
78
|
+
const changed = (await git(["diff", "--name-only", before, "refs/heads/dream/new-project"], remote)).stdout.trim().split("\n");
|
|
79
|
+
assert.deepEqual(changed, ["projects/new-project/current_state.md", "projects/new-project/open_threads.md"]);
|
|
80
|
+
assert.match((await git(["show", "dream/new-project:projects/new-project/current_state.md"], remote)).stdout, /hippocamp-dream-through:/);
|
|
81
|
+
assert.equal(await fs.readFile(calls, "utf8"), "created\n");
|
|
82
|
+
// Simulate accepting the reviewed PR, then repeat against the same evidence.
|
|
83
|
+
await git(["update-ref", "refs/heads/main", "refs/heads/dream/new-project"], remote);
|
|
84
|
+
await exec("bash", [runner], { env });
|
|
85
|
+
assert.equal(await fs.readFile(calls, "utf8"), "created\n");
|
|
86
|
+
});
|
|
@@ -63,3 +63,57 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
|
|
|
63
63
|
/escapes the memory root/,
|
|
64
64
|
);
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
test("sync pushes commits left behind by an earlier failed push", async (t) => {
|
|
68
|
+
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
69
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-backlog-"));
|
|
70
|
+
const remote = path.join(tempRoot, "remote.git");
|
|
71
|
+
const lagoon = path.join(tempRoot, "lagoon");
|
|
72
|
+
const projectRoot = path.join(tempRoot, "project");
|
|
73
|
+
const git = (args, cwd = lagoon) => execFileSync("git", args, { cwd }).toString().trim();
|
|
74
|
+
|
|
75
|
+
const identity = { GIT_AUTHOR_NAME: "Test", GIT_AUTHOR_EMAIL: "test@example.com", GIT_COMMITTER_NAME: "Test", GIT_COMMITTER_EMAIL: "test@example.com" };
|
|
76
|
+
const saved = Object.fromEntries(Object.keys(identity).map((key) => [key, process.env[key]]));
|
|
77
|
+
Object.assign(process.env, identity);
|
|
78
|
+
t.after(() => {
|
|
79
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
delete process.env[key];
|
|
82
|
+
} else {
|
|
83
|
+
process.env[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
git(["init", "-q", "--bare", "-b", "main", remote], tempRoot);
|
|
89
|
+
git(["clone", "-q", remote, lagoon], tempRoot);
|
|
90
|
+
git(["commit", "-q", "--allow-empty", "-m", "init"]);
|
|
91
|
+
git(["push", "-q", "-u", "origin", "main"]);
|
|
92
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = lagoon;
|
|
93
|
+
t.after(async () => {
|
|
94
|
+
if (originalRoot === undefined) {
|
|
95
|
+
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
96
|
+
} else {
|
|
97
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
98
|
+
}
|
|
99
|
+
await fs.rm(tempRoot, { recursive: true, force: true, maxRetries: 5 });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Simulate an unreachable remote for the first write.
|
|
103
|
+
git(["remote", "set-url", "origin", path.join(tempRoot, "missing.git")]);
|
|
104
|
+
const write = await memory.appendEvent({ content: "Use HTTPS.", keywords: ["sync"], projectRoot, scope: "project", title: "Remote" });
|
|
105
|
+
assert.equal(write.sync.committed, true);
|
|
106
|
+
assert.equal(write.sync.pushed, false);
|
|
107
|
+
|
|
108
|
+
git(["remote", "set-url", "origin", remote]);
|
|
109
|
+
const result = await memory.syncMemory({
|
|
110
|
+
scope: "project",
|
|
111
|
+
projectRoot,
|
|
112
|
+
paths: await memory.getDefaultSyncPaths("project", projectRoot),
|
|
113
|
+
message: "sync",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.equal(result.committed, false);
|
|
117
|
+
assert.equal(result.pushed, true);
|
|
118
|
+
assert.equal(git(["rev-parse", "main"], remote), git(["rev-parse", "HEAD"]));
|
|
119
|
+
});
|