@hunsu/core 0.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/LICENSE +157 -0
- package/dist/artifact-actions.d.ts +83 -0
- package/dist/artifact-actions.js +415 -0
- package/dist/board.d.ts +15 -0
- package/dist/board.js +223 -0
- package/dist/domain-store.d.ts +268 -0
- package/dist/domain-store.js +951 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +34 -0
- package/dist/git.d.ts +105 -0
- package/dist/git.js +323 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lookup.d.ts +7 -0
- package/dist/lookup.js +57 -0
- package/dist/model.d.ts +71 -0
- package/dist/model.js +18 -0
- package/dist/port.d.ts +56 -0
- package/dist/port.js +186 -0
- package/dist/primitives.d.ts +48 -0
- package/dist/primitives.js +108 -0
- package/dist/trailer.d.ts +7 -0
- package/dist/trailer.js +67 -0
- package/package.json +33 -0
- package/src/artifact-actions.ts +531 -0
- package/src/board.ts +272 -0
- package/src/domain-store.ts +1263 -0
- package/src/errors.ts +39 -0
- package/src/git.ts +445 -0
- package/src/index.ts +10 -0
- package/src/lookup.ts +72 -0
- package/src/model.ts +109 -0
- package/src/port.ts +263 -0
- package/src/primitives.ts +160 -0
- package/src/trailer.ts +79 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class HunsuError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class InvalidTrailerError extends HunsuError {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class MissingTrailerError extends HunsuError {
|
|
8
|
+
constructor(commitSha: string, trailer: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class LookupError extends HunsuError {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class GitError extends HunsuError {
|
|
14
|
+
readonly command: string[];
|
|
15
|
+
readonly stderr: string;
|
|
16
|
+
constructor(command: string[], stderr: string);
|
|
17
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class HunsuError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "HunsuError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class InvalidTrailerError extends HunsuError {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "InvalidTrailerError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class MissingTrailerError extends HunsuError {
|
|
14
|
+
constructor(commitSha, trailer) {
|
|
15
|
+
super(`Commit ${commitSha} is missing required trailer ${trailer}`);
|
|
16
|
+
this.name = "MissingTrailerError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class LookupError extends HunsuError {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "LookupError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class GitError extends HunsuError {
|
|
26
|
+
command;
|
|
27
|
+
stderr;
|
|
28
|
+
constructor(command, stderr) {
|
|
29
|
+
super(`Git command failed: git ${command.join(" ")}${stderr ? `\n${stderr}` : ""}`);
|
|
30
|
+
this.name = "GitError";
|
|
31
|
+
this.command = command;
|
|
32
|
+
this.stderr = stderr;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Board, CommitRecord, MoveEvent, MoveEventStatus } from "./model.ts";
|
|
2
|
+
import type { CommitSha, GitRef } from "./primitives.ts";
|
|
3
|
+
type GitOptions = {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
input?: string;
|
|
6
|
+
env?: Record<string, string | undefined>;
|
|
7
|
+
};
|
|
8
|
+
export type GitLogOptions = {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
refs?: string[];
|
|
11
|
+
};
|
|
12
|
+
export type HunsuCommitOptions = {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
id: string;
|
|
15
|
+
targetMove: MoveEvent;
|
|
16
|
+
newRun: string;
|
|
17
|
+
artifactText: string;
|
|
18
|
+
actor?: string;
|
|
19
|
+
role?: string;
|
|
20
|
+
priority?: string;
|
|
21
|
+
};
|
|
22
|
+
export type HunsuCommitResult = {
|
|
23
|
+
commitSha: CommitSha;
|
|
24
|
+
markerRef: GitRef;
|
|
25
|
+
runRef: GitRef;
|
|
26
|
+
artifactPath: string;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
export type MoveCommitResult = {
|
|
30
|
+
commitSha: CommitSha;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
export type FinalizedMoveCommitOptions = {
|
|
34
|
+
cwd?: string;
|
|
35
|
+
runId: string;
|
|
36
|
+
moveId: string;
|
|
37
|
+
goal: string;
|
|
38
|
+
finalizerMessage: string;
|
|
39
|
+
actor?: string;
|
|
40
|
+
};
|
|
41
|
+
export type MemberPathCommitOptions = {
|
|
42
|
+
cwd?: string;
|
|
43
|
+
runId: string;
|
|
44
|
+
pathId: string;
|
|
45
|
+
executorId: string;
|
|
46
|
+
goal: string;
|
|
47
|
+
requires: string[] | "PrevMove";
|
|
48
|
+
paths?: string[];
|
|
49
|
+
};
|
|
50
|
+
export type MemberPathCommitResult = {
|
|
51
|
+
commitSha: CommitSha;
|
|
52
|
+
parentSha: CommitSha;
|
|
53
|
+
treeSha: string;
|
|
54
|
+
parentTreeSha: string;
|
|
55
|
+
treeChanged: boolean;
|
|
56
|
+
message: string;
|
|
57
|
+
};
|
|
58
|
+
export type SquashMoveCommitOptions = {
|
|
59
|
+
cwd?: string;
|
|
60
|
+
fromRef: string;
|
|
61
|
+
runRef: string;
|
|
62
|
+
message: string;
|
|
63
|
+
};
|
|
64
|
+
export type SquashMoveCommitResult = {
|
|
65
|
+
commitSha: CommitSha;
|
|
66
|
+
parentSha: CommitSha;
|
|
67
|
+
fromSha: CommitSha;
|
|
68
|
+
runRef: GitRef;
|
|
69
|
+
message: string;
|
|
70
|
+
};
|
|
71
|
+
export declare function git(args: string[], options?: GitOptions): string;
|
|
72
|
+
export declare function ensureGitRepository(cwd?: string): string;
|
|
73
|
+
export declare function readCommitRecords(options?: GitLogOptions): CommitRecord[];
|
|
74
|
+
export declare function loadBoardFromGit(options?: GitLogOptions): Board;
|
|
75
|
+
export declare function buildMoveCommitMessage(input: {
|
|
76
|
+
goal: string;
|
|
77
|
+
run: string;
|
|
78
|
+
move: string | number;
|
|
79
|
+
summary?: string;
|
|
80
|
+
why?: string;
|
|
81
|
+
alternatives?: string;
|
|
82
|
+
evidence?: string;
|
|
83
|
+
risks?: string;
|
|
84
|
+
next?: string;
|
|
85
|
+
role?: string;
|
|
86
|
+
actor?: string;
|
|
87
|
+
status?: MoveEventStatus;
|
|
88
|
+
}): string;
|
|
89
|
+
export declare function createMoveCommit(message: string, options?: {
|
|
90
|
+
cwd?: string;
|
|
91
|
+
}): MoveCommitResult;
|
|
92
|
+
export declare function createMoveCommitFromWorktree(message: string, options?: {
|
|
93
|
+
cwd?: string;
|
|
94
|
+
}): MoveCommitResult;
|
|
95
|
+
export declare function buildFinalizedMoveCommitMessage(input: FinalizedMoveCommitOptions): string;
|
|
96
|
+
export declare function createFinalizedMoveCommit(options: FinalizedMoveCommitOptions): MoveCommitResult;
|
|
97
|
+
export declare function buildMemberPathCommitMessage(input: MemberPathCommitOptions): string;
|
|
98
|
+
export declare function createMemberPathCommitFromWorktree(options: MemberPathCommitOptions): MemberPathCommitResult;
|
|
99
|
+
export declare function createSquashMoveCommit(options: SquashMoveCommitOptions): SquashMoveCommitResult;
|
|
100
|
+
export declare function buildHunsuCommitMessage(options: HunsuCommitOptions): string;
|
|
101
|
+
export declare function createHunsuCommit(options: HunsuCommitOptions): HunsuCommitResult;
|
|
102
|
+
export declare function updateMainToMove(move: MoveEvent, options?: {
|
|
103
|
+
cwd?: string;
|
|
104
|
+
}): string;
|
|
105
|
+
export {};
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { GitError, HunsuError } from "./errors.js";
|
|
5
|
+
import { createBoardFromCommits } from "./board.js";
|
|
6
|
+
import { parseCommitSha, parseGitRef } from "./primitives.js";
|
|
7
|
+
import { serializeTrailers } from "./trailer.js";
|
|
8
|
+
const RECORD_SEPARATOR = "\x1e";
|
|
9
|
+
const FIELD_SEPARATOR = "\x1f";
|
|
10
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
11
|
+
const LOG_FORMAT = [
|
|
12
|
+
"%H",
|
|
13
|
+
"%h",
|
|
14
|
+
"%P",
|
|
15
|
+
"%D",
|
|
16
|
+
"%an",
|
|
17
|
+
"%aI",
|
|
18
|
+
"%s",
|
|
19
|
+
"%B"
|
|
20
|
+
].join(FIELD_SEPARATOR);
|
|
21
|
+
export function git(args, options = {}) {
|
|
22
|
+
const result = spawnSync("git", args, {
|
|
23
|
+
cwd: options.cwd,
|
|
24
|
+
input: options.input,
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
27
|
+
env: options.env
|
|
28
|
+
});
|
|
29
|
+
if (result.status !== 0) {
|
|
30
|
+
throw new GitError(args, result.stderr.trim());
|
|
31
|
+
}
|
|
32
|
+
return result.stdout;
|
|
33
|
+
}
|
|
34
|
+
export function ensureGitRepository(cwd = process.cwd()) {
|
|
35
|
+
return git(["rev-parse", "--show-toplevel"], { cwd }).trim();
|
|
36
|
+
}
|
|
37
|
+
export function readCommitRecords(options = {}) {
|
|
38
|
+
const hasCommit = git(["rev-list", "--all", "--max-count=1"], { cwd: options.cwd }).trim();
|
|
39
|
+
if (!hasCommit) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const refs = options.refs && options.refs.length > 0 ? options.refs : ["--all"];
|
|
43
|
+
const output = git(["log", "--date-order", `--format=${RECORD_SEPARATOR}${LOG_FORMAT}`, ...refs], {
|
|
44
|
+
cwd: options.cwd
|
|
45
|
+
});
|
|
46
|
+
return output
|
|
47
|
+
.split(RECORD_SEPARATOR)
|
|
48
|
+
.filter(record => record.trim() !== "")
|
|
49
|
+
.map(parseLogRecord);
|
|
50
|
+
}
|
|
51
|
+
export function loadBoardFromGit(options = {}) {
|
|
52
|
+
return createBoardFromCommits(readCommitRecords(options));
|
|
53
|
+
}
|
|
54
|
+
export function buildMoveCommitMessage(input) {
|
|
55
|
+
const subject = `move: ${input.summary ?? input.goal}`;
|
|
56
|
+
const body = [
|
|
57
|
+
`Goal: ${input.goal}`,
|
|
58
|
+
input.why ? `Why: ${input.why}` : undefined,
|
|
59
|
+
input.alternatives ? `Alternatives: ${input.alternatives}` : undefined,
|
|
60
|
+
input.evidence ? `Evidence: ${input.evidence}` : undefined,
|
|
61
|
+
input.risks ? `Risks: ${input.risks}` : undefined,
|
|
62
|
+
input.next ? `Next: ${input.next}` : undefined
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
const trailers = serializeTrailers({
|
|
65
|
+
"Hunsu-Event": "move",
|
|
66
|
+
"Hunsu-Run": input.run,
|
|
67
|
+
"Hunsu-Move": input.move,
|
|
68
|
+
"Hunsu-Goal": input.goal,
|
|
69
|
+
"Hunsu-Role": input.role ?? "team",
|
|
70
|
+
"Hunsu-Actor": input.actor ?? "agent:codex",
|
|
71
|
+
"Hunsu-Status": input.status ?? "complete"
|
|
72
|
+
});
|
|
73
|
+
return `${subject}\n\n${body.join("\n")}\n\n${trailers}\n`;
|
|
74
|
+
}
|
|
75
|
+
export function createMoveCommit(message, options = {}) {
|
|
76
|
+
const cwd = options.cwd ?? process.cwd();
|
|
77
|
+
const tempDir = createRepoTempDir(cwd, "hunsu-move-");
|
|
78
|
+
const messageFile = join(tempDir, "message.txt");
|
|
79
|
+
try {
|
|
80
|
+
writeFileSync(messageFile, message, "utf8");
|
|
81
|
+
git(["commit", "-F", messageFile], { cwd });
|
|
82
|
+
const commitSha = parseCommitSha(git(["rev-parse", "HEAD"], { cwd }).trim());
|
|
83
|
+
return { commitSha, message };
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function createMoveCommitFromWorktree(message, options = {}) {
|
|
90
|
+
const cwd = options.cwd ?? process.cwd();
|
|
91
|
+
const changes = git(["status", "--porcelain=v1"], { cwd }).trim();
|
|
92
|
+
if (!changes) {
|
|
93
|
+
throw new HunsuError("Cannot create MOVE commit without worktree changes");
|
|
94
|
+
}
|
|
95
|
+
git(["add", "--all"], { cwd });
|
|
96
|
+
return createMoveCommit(message, { cwd });
|
|
97
|
+
}
|
|
98
|
+
export function buildFinalizedMoveCommitMessage(input) {
|
|
99
|
+
const message = input.finalizerMessage.trim();
|
|
100
|
+
if (!message) {
|
|
101
|
+
throw new HunsuError("MOVE finalizer returned an empty commit message");
|
|
102
|
+
}
|
|
103
|
+
const trailers = serializeTrailers({
|
|
104
|
+
"Hunsu-Event": "move",
|
|
105
|
+
"Hunsu-Run": input.runId,
|
|
106
|
+
"Hunsu-Move": input.moveId,
|
|
107
|
+
"Hunsu-Goal": input.goal,
|
|
108
|
+
"Hunsu-Role": "move-finalizer",
|
|
109
|
+
"Hunsu-Actor": input.actor ?? "agent:codex",
|
|
110
|
+
"Hunsu-Status": "complete"
|
|
111
|
+
});
|
|
112
|
+
return `${message}\n\n${trailers}\n`;
|
|
113
|
+
}
|
|
114
|
+
export function createFinalizedMoveCommit(options) {
|
|
115
|
+
const cwd = options.cwd ?? process.cwd();
|
|
116
|
+
const tempDir = createRepoTempDir(cwd, "hunsu-final-move-");
|
|
117
|
+
const messageFile = join(tempDir, "message.txt");
|
|
118
|
+
const message = buildFinalizedMoveCommitMessage(options);
|
|
119
|
+
try {
|
|
120
|
+
writeFileSync(messageFile, message, "utf8");
|
|
121
|
+
git(["commit", "--allow-empty", "-F", messageFile], { cwd });
|
|
122
|
+
const commitSha = parseCommitSha(git(["rev-parse", "--verify", "HEAD"], { cwd }).trim());
|
|
123
|
+
return { commitSha, message };
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function buildMemberPathCommitMessage(input) {
|
|
130
|
+
const requires = Array.isArray(input.requires) ? input.requires.join(",") : input.requires;
|
|
131
|
+
const trailers = serializeTrailers({
|
|
132
|
+
"Hunsu-Event": "member-path",
|
|
133
|
+
"Hunsu-Run": input.runId,
|
|
134
|
+
"Hunsu-Path": input.pathId,
|
|
135
|
+
"Hunsu-Member": input.executorId,
|
|
136
|
+
"Hunsu-Goal": input.goal,
|
|
137
|
+
"Hunsu-Requires": requires
|
|
138
|
+
});
|
|
139
|
+
return [
|
|
140
|
+
`path: ${input.pathId} (${input.executorId})`,
|
|
141
|
+
"",
|
|
142
|
+
`Goal: ${input.goal}`,
|
|
143
|
+
"",
|
|
144
|
+
`Requires: ${requires}`,
|
|
145
|
+
"",
|
|
146
|
+
trailers
|
|
147
|
+
].join("\n") + "\n";
|
|
148
|
+
}
|
|
149
|
+
export function createMemberPathCommitFromWorktree(options) {
|
|
150
|
+
const cwd = options.cwd ?? process.cwd();
|
|
151
|
+
const tempDir = createRepoTempDir(cwd, "hunsu-path-");
|
|
152
|
+
const messageFile = join(tempDir, "message.txt");
|
|
153
|
+
const message = buildMemberPathCommitMessage(options);
|
|
154
|
+
try {
|
|
155
|
+
const parentSha = ensureCommitParent(cwd);
|
|
156
|
+
const parentTreeSha = git(["rev-parse", "--verify", `${parentSha}^{tree}`], { cwd }).trim();
|
|
157
|
+
writeFileSync(messageFile, message, "utf8");
|
|
158
|
+
const paths = options.paths?.filter(path => path.trim() !== "");
|
|
159
|
+
if (paths && paths.length > 0) {
|
|
160
|
+
git(["add", "--all", "--", ...paths], { cwd });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
git(["add", "--all"], { cwd });
|
|
164
|
+
}
|
|
165
|
+
git(["commit", "--allow-empty", "-F", messageFile], { cwd });
|
|
166
|
+
const commitSha = parseCommitSha(git(["rev-parse", "--verify", "HEAD"], { cwd }).trim());
|
|
167
|
+
const treeSha = git(["rev-parse", "--verify", `${commitSha}^{tree}`], { cwd }).trim();
|
|
168
|
+
return {
|
|
169
|
+
commitSha,
|
|
170
|
+
parentSha,
|
|
171
|
+
treeSha,
|
|
172
|
+
parentTreeSha,
|
|
173
|
+
treeChanged: treeSha !== parentTreeSha,
|
|
174
|
+
message
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function ensureCommitParent(cwd) {
|
|
182
|
+
try {
|
|
183
|
+
return parseCommitSha(git(["rev-parse", "--verify", "HEAD"], { cwd }).trim());
|
|
184
|
+
}
|
|
185
|
+
catch (_error) {
|
|
186
|
+
git(["commit", "--allow-empty", "-m", "hunsu: initialize repository worktree"], { cwd });
|
|
187
|
+
return parseCommitSha(git(["rev-parse", "--verify", "HEAD"], { cwd }).trim());
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function createSquashMoveCommit(options) {
|
|
191
|
+
const cwd = options.cwd ?? process.cwd();
|
|
192
|
+
const tempDir = createRepoTempDir(cwd, "hunsu-squash-");
|
|
193
|
+
const messageFile = join(tempDir, "message.txt");
|
|
194
|
+
const runHeadRef = parseGitRef(toHeadRef(options.runRef));
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(messageFile, options.message, "utf8");
|
|
197
|
+
const parentSha = parseCommitSha(git(["rev-parse", "--verify", options.runRef], { cwd }).trim());
|
|
198
|
+
const fromSha = parseCommitSha(git(["rev-parse", "--verify", options.fromRef], { cwd }).trim());
|
|
199
|
+
assertAncestor(parentSha, fromSha, cwd);
|
|
200
|
+
const tree = git(["rev-parse", `${fromSha}^{tree}`], { cwd }).trim();
|
|
201
|
+
const commitSha = parseCommitSha(git(["commit-tree", tree, "-p", parentSha, "-F", messageFile], { cwd }).trim());
|
|
202
|
+
git(["update-ref", runHeadRef, commitSha, parentSha], { cwd });
|
|
203
|
+
return { commitSha, parentSha, fromSha, runRef: runHeadRef, message: options.message };
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export function buildHunsuCommitMessage(options) {
|
|
210
|
+
const firstLine = options.artifactText
|
|
211
|
+
.split(/\r?\n/)
|
|
212
|
+
.map(line => line.trim())
|
|
213
|
+
.find(line => line.length > 0);
|
|
214
|
+
const title = firstLine?.replace(/^#+\s*/, "") || `intervention on ${options.targetMove.moveId}`;
|
|
215
|
+
const trailers = serializeTrailers({
|
|
216
|
+
"Hunsu-Event": "hunsu",
|
|
217
|
+
"Hunsu-ID": options.id,
|
|
218
|
+
"Hunsu-Target": options.targetMove.commit.sha,
|
|
219
|
+
"Hunsu-Source-Run": options.targetMove.runId,
|
|
220
|
+
"Hunsu-New-Run": options.newRun,
|
|
221
|
+
"Hunsu-Role": options.role ?? "director",
|
|
222
|
+
"Hunsu-Actor": options.actor ?? "human",
|
|
223
|
+
"Hunsu-Priority": options.priority
|
|
224
|
+
});
|
|
225
|
+
return `hunsu: ${title}\n\n${options.artifactText.trim()}\n\n${trailers}\n`;
|
|
226
|
+
}
|
|
227
|
+
export function createHunsuCommit(options) {
|
|
228
|
+
const cwd = options.cwd ?? process.cwd();
|
|
229
|
+
const message = buildHunsuCommitMessage(options);
|
|
230
|
+
const artifactPath = `.hunsu/hunsus/${options.id}.md`;
|
|
231
|
+
const runRef = parseGitRef(`refs/heads/${options.newRun}`);
|
|
232
|
+
const markerRef = parseGitRef(`refs/heads/hunsu/${options.id}/from/${options.targetMove.commit.shortSha}`);
|
|
233
|
+
const indexDir = createRepoTempDir(cwd, "hunsu-index-");
|
|
234
|
+
const indexPath = join(indexDir, "index");
|
|
235
|
+
const artifactFile = join(indexDir, "artifact.md");
|
|
236
|
+
const messageFile = join(indexDir, "message.txt");
|
|
237
|
+
const env = { GIT_INDEX_FILE: indexPath };
|
|
238
|
+
try {
|
|
239
|
+
assertRefMissing(runRef, cwd);
|
|
240
|
+
assertRefMissing(markerRef, cwd);
|
|
241
|
+
writeFileSync(artifactFile, `${options.artifactText.trim()}\n`, "utf8");
|
|
242
|
+
writeFileSync(messageFile, message, "utf8");
|
|
243
|
+
git(["read-tree", `${options.targetMove.commit.sha}^{tree}`], { cwd, env });
|
|
244
|
+
const blob = git(["hash-object", "-w", artifactFile], { cwd }).trim();
|
|
245
|
+
git(["update-index", "--add", "--cacheinfo", `100644,${blob},${artifactPath}`], { cwd, env });
|
|
246
|
+
const tree = git(["write-tree"], { cwd, env }).trim();
|
|
247
|
+
const commitSha = parseCommitSha(git(["commit-tree", tree, "-p", options.targetMove.commit.sha, "-F", messageFile], { cwd }).trim());
|
|
248
|
+
git(["update-ref", runRef, commitSha], { cwd });
|
|
249
|
+
git(["update-ref", markerRef, commitSha], { cwd });
|
|
250
|
+
return { commitSha, markerRef, runRef, artifactPath, message };
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
rmSync(indexDir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export function updateMainToMove(move, options = {}) {
|
|
257
|
+
git(["update-ref", "refs/heads/main", move.commit.sha], { cwd: options.cwd });
|
|
258
|
+
return move.commit.sha;
|
|
259
|
+
}
|
|
260
|
+
function assertRefMissing(ref, cwd) {
|
|
261
|
+
const result = spawnSync("git", ["show-ref", "--verify", "--quiet", ref], {
|
|
262
|
+
cwd,
|
|
263
|
+
encoding: "utf8"
|
|
264
|
+
});
|
|
265
|
+
if (result.status === 0) {
|
|
266
|
+
throw new HunsuError(`Ref already exists: ${ref}`);
|
|
267
|
+
}
|
|
268
|
+
if (result.status !== 1) {
|
|
269
|
+
throw new GitError(["show-ref", "--verify", "--quiet", ref], result.stderr.trim());
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function createRepoTempDir(cwd, prefix) {
|
|
273
|
+
const gitPath = git(["rev-parse", "--git-path", "hunsu/tmp"], { cwd }).trim();
|
|
274
|
+
const tempRoot = isAbsolute(gitPath) ? gitPath : join(cwd, gitPath);
|
|
275
|
+
mkdirSync(tempRoot, { recursive: true });
|
|
276
|
+
return mkdtempSync(join(tempRoot, prefix));
|
|
277
|
+
}
|
|
278
|
+
function assertAncestor(ancestorSha, descendantSha, cwd) {
|
|
279
|
+
const result = spawnSync("git", ["merge-base", "--is-ancestor", ancestorSha, descendantSha], {
|
|
280
|
+
cwd,
|
|
281
|
+
encoding: "utf8"
|
|
282
|
+
});
|
|
283
|
+
if (result.status === 0) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (result.status === 1) {
|
|
287
|
+
throw new HunsuError(`${ancestorSha} is not an ancestor of ${descendantSha}`);
|
|
288
|
+
}
|
|
289
|
+
throw new GitError(["merge-base", "--is-ancestor", ancestorSha, descendantSha], result.stderr.trim());
|
|
290
|
+
}
|
|
291
|
+
function toHeadRef(ref) {
|
|
292
|
+
return ref.startsWith("refs/") ? ref : `refs/heads/${ref}`;
|
|
293
|
+
}
|
|
294
|
+
function parseLogRecord(record) {
|
|
295
|
+
const fields = splitFields(record, 8);
|
|
296
|
+
const [sha, shortSha, parents, refs, authorName, authoredAt, subject, body] = fields;
|
|
297
|
+
return {
|
|
298
|
+
sha: parseCommitSha(sha),
|
|
299
|
+
shortSha,
|
|
300
|
+
parents: parents ? parents.split(" ") : [],
|
|
301
|
+
refs: refs ? refs.split(", ").filter(Boolean).map(ref => parseGitRef(ref)) : [],
|
|
302
|
+
authorName,
|
|
303
|
+
authoredAt,
|
|
304
|
+
subject,
|
|
305
|
+
body
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function splitFields(record, count) {
|
|
309
|
+
const fields = [];
|
|
310
|
+
let remainder = record;
|
|
311
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
312
|
+
const separator = remainder.indexOf(FIELD_SEPARATOR);
|
|
313
|
+
if (separator === -1) {
|
|
314
|
+
fields.push(remainder);
|
|
315
|
+
remainder = "";
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
fields.push(remainder.slice(0, separator));
|
|
319
|
+
remainder = remainder.slice(separator + 1);
|
|
320
|
+
}
|
|
321
|
+
fields.push(remainder);
|
|
322
|
+
return fields;
|
|
323
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./board.ts";
|
|
2
|
+
export * from "./domain-store.ts";
|
|
3
|
+
export * from "./errors.ts";
|
|
4
|
+
export * from "./git.ts";
|
|
5
|
+
export * from "./lookup.ts";
|
|
6
|
+
export * from "./model.ts";
|
|
7
|
+
export * from "./port.ts";
|
|
8
|
+
export * from "./primitives.ts";
|
|
9
|
+
export * from "./artifact-actions.ts";
|
|
10
|
+
export * from "./trailer.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./board.js";
|
|
2
|
+
export * from "./domain-store.js";
|
|
3
|
+
export * from "./errors.js";
|
|
4
|
+
export * from "./git.js";
|
|
5
|
+
export * from "./lookup.js";
|
|
6
|
+
export * from "./model.js";
|
|
7
|
+
export * from "./port.js";
|
|
8
|
+
export * from "./primitives.js";
|
|
9
|
+
export * from "./artifact-actions.js";
|
|
10
|
+
export * from "./trailer.js";
|
package/dist/lookup.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Board, MoveEvent } from "./model.ts";
|
|
2
|
+
export type LookupOptions = {
|
|
3
|
+
runId?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function resolveMove(board: Board, selector: string, options?: LookupOptions): MoveEvent;
|
|
6
|
+
export declare function nextHunsuId(board: Board): string;
|
|
7
|
+
export declare function defaultHunsuRun(sourceRun: string, hunsuId: string): string;
|
package/dist/lookup.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { LookupError } from "./errors.js";
|
|
2
|
+
import { formatMoveId } from "./trailer.js";
|
|
3
|
+
export function resolveMove(board, selector, options = {}) {
|
|
4
|
+
const normalized = normalizeSelector(selector);
|
|
5
|
+
const candidates = board.moves.filter(move => {
|
|
6
|
+
if (options.runId && move.runId !== options.runId) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return (move.commit.sha === selector ||
|
|
10
|
+
move.commit.sha.startsWith(selector) ||
|
|
11
|
+
move.commit.shortSha === selector ||
|
|
12
|
+
move.moveId === normalized ||
|
|
13
|
+
move.moveNumber === selector ||
|
|
14
|
+
`${move.runId}:${move.moveId}` === selector);
|
|
15
|
+
});
|
|
16
|
+
if (candidates.length === 0) {
|
|
17
|
+
throw new LookupError(`No move found for ${selector}`);
|
|
18
|
+
}
|
|
19
|
+
const unique = dedupeBySha(candidates);
|
|
20
|
+
if (unique.length > 1) {
|
|
21
|
+
const choices = unique.map(move => `${move.runId}:${move.moveId}@${move.commit.shortSha}`).join(", ");
|
|
22
|
+
throw new LookupError(`Ambiguous move selector ${selector}: ${choices}`);
|
|
23
|
+
}
|
|
24
|
+
return unique[0];
|
|
25
|
+
}
|
|
26
|
+
export function nextHunsuId(board) {
|
|
27
|
+
const highest = board.hunsus.reduce((max, hunsu) => {
|
|
28
|
+
const match = hunsu.hunsuId.match(/^h(\d+)$/i);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return max;
|
|
31
|
+
}
|
|
32
|
+
return Math.max(max, Number(match[1]));
|
|
33
|
+
}, 0);
|
|
34
|
+
return `h${String(highest + 1).padStart(3, "0")}`;
|
|
35
|
+
}
|
|
36
|
+
export function defaultHunsuRun(sourceRun, hunsuId) {
|
|
37
|
+
const suffix = hunsuId.replace(/^h/i, "h");
|
|
38
|
+
return `${sourceRun}-${suffix}`;
|
|
39
|
+
}
|
|
40
|
+
function normalizeSelector(selector) {
|
|
41
|
+
if (/^M?\d+$/i.test(selector)) {
|
|
42
|
+
return formatMoveId(selector);
|
|
43
|
+
}
|
|
44
|
+
return selector;
|
|
45
|
+
}
|
|
46
|
+
function dedupeBySha(moves) {
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const unique = [];
|
|
49
|
+
for (const move of moves) {
|
|
50
|
+
if (seen.has(move.commit.sha)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
seen.add(move.commit.sha);
|
|
54
|
+
unique.push(move);
|
|
55
|
+
}
|
|
56
|
+
return unique;
|
|
57
|
+
}
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CommitSha, GitGoal, GitHunsuId, GitMoveId, GitMoveNumber, GitPriority, GitRef, GitRunId, GitTrailerActor, GitTrailerRole } from "./primitives.ts";
|
|
2
|
+
export type HunsuEventType = "move" | "hunsu";
|
|
3
|
+
export type MoveEventStatus = "complete" | "failed" | "blocked";
|
|
4
|
+
export type TrailerMap = Map<string, string[]>;
|
|
5
|
+
export type CommitRecord = {
|
|
6
|
+
sha: CommitSha;
|
|
7
|
+
shortSha: string;
|
|
8
|
+
parents: string[];
|
|
9
|
+
refs: GitRef[];
|
|
10
|
+
authorName: string;
|
|
11
|
+
authoredAt: string;
|
|
12
|
+
subject: string;
|
|
13
|
+
body: string;
|
|
14
|
+
};
|
|
15
|
+
export type MoveEvent = {
|
|
16
|
+
type: "move";
|
|
17
|
+
commit: CommitRecord;
|
|
18
|
+
runId: GitRunId;
|
|
19
|
+
moveNumber: GitMoveNumber;
|
|
20
|
+
moveId: GitMoveId;
|
|
21
|
+
goal: GitGoal;
|
|
22
|
+
role: GitTrailerRole;
|
|
23
|
+
actor: GitTrailerActor;
|
|
24
|
+
status: MoveEventStatus;
|
|
25
|
+
trailers: TrailerMap;
|
|
26
|
+
};
|
|
27
|
+
export type HunsuEvent = {
|
|
28
|
+
type: "hunsu";
|
|
29
|
+
commit: CommitRecord;
|
|
30
|
+
hunsuId: GitHunsuId;
|
|
31
|
+
target: CommitSha;
|
|
32
|
+
sourceRun: GitRunId;
|
|
33
|
+
newRun: GitRunId;
|
|
34
|
+
role: GitTrailerRole;
|
|
35
|
+
actor: GitTrailerActor;
|
|
36
|
+
priority?: GitPriority;
|
|
37
|
+
trailers: TrailerMap;
|
|
38
|
+
};
|
|
39
|
+
export type BoardEvent = MoveEvent | HunsuEvent;
|
|
40
|
+
export type RunTimeline = {
|
|
41
|
+
runId: string;
|
|
42
|
+
moves: MoveEvent[];
|
|
43
|
+
hunsus: HunsuEvent[];
|
|
44
|
+
events: BoardEvent[];
|
|
45
|
+
};
|
|
46
|
+
export type Board = {
|
|
47
|
+
runs: RunTimeline[];
|
|
48
|
+
moves: MoveEvent[];
|
|
49
|
+
hunsus: HunsuEvent[];
|
|
50
|
+
positions: Map<string, MoveEvent>;
|
|
51
|
+
};
|
|
52
|
+
export type HunsuConfig = {
|
|
53
|
+
runtime: {
|
|
54
|
+
default_runner: string;
|
|
55
|
+
max_goal_minutes: number;
|
|
56
|
+
};
|
|
57
|
+
git: {
|
|
58
|
+
require_trailers: boolean;
|
|
59
|
+
main_mode: string;
|
|
60
|
+
allow_write_main: boolean;
|
|
61
|
+
};
|
|
62
|
+
members: {
|
|
63
|
+
default: string[];
|
|
64
|
+
verification: string[];
|
|
65
|
+
};
|
|
66
|
+
approval: {
|
|
67
|
+
require_human_for: string[];
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export declare const REQUIRED_MOVE_TRAILERS: string[];
|
|
71
|
+
export declare const REQUIRED_HUNSU_TRAILERS: string[];
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const REQUIRED_MOVE_TRAILERS = [
|
|
2
|
+
"Hunsu-Event",
|
|
3
|
+
"Hunsu-Run",
|
|
4
|
+
"Hunsu-Move",
|
|
5
|
+
"Hunsu-Goal",
|
|
6
|
+
"Hunsu-Role",
|
|
7
|
+
"Hunsu-Actor",
|
|
8
|
+
"Hunsu-Status"
|
|
9
|
+
];
|
|
10
|
+
export const REQUIRED_HUNSU_TRAILERS = [
|
|
11
|
+
"Hunsu-Event",
|
|
12
|
+
"Hunsu-ID",
|
|
13
|
+
"Hunsu-Target",
|
|
14
|
+
"Hunsu-Source-Run",
|
|
15
|
+
"Hunsu-New-Run",
|
|
16
|
+
"Hunsu-Role",
|
|
17
|
+
"Hunsu-Actor"
|
|
18
|
+
];
|