@yegor256/jinka 0.0.7 → 0.0.9
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/bin/jinka.js +66 -48
- package/package.json +1 -1
- package/src/agent.js +4 -7
- package/src/archived.js +22 -30
- package/src/claude.js +14 -24
- package/src/codex.js +17 -27
- package/src/colorless.js +13 -0
- package/src/crew.js +56 -61
- package/src/disk.js +20 -28
- package/src/duration.js +26 -26
- package/src/ellipsized.js +10 -0
- package/src/event.js +76 -82
- package/src/freeze.js +36 -41
- package/src/frozen.js +19 -21
- package/src/genie.js +18 -23
- package/src/jinka.js +7 -13
- package/src/jobs.js +13 -16
- package/src/keys.js +19 -23
- package/src/legend.js +9 -13
- package/src/limited.js +12 -15
- package/src/log.js +8 -17
- package/src/logo.js +3 -5
- package/src/meters.js +3 -8
- package/src/model.js +20 -20
- package/src/permanent.js +9 -21
- package/src/protocol.js +4 -11
- package/src/quota.js +22 -32
- package/src/release.js +30 -0
- package/src/rollouts.js +38 -40
- package/src/row.js +3 -9
- package/src/semaphore.js +16 -0
- package/src/status.js +10 -18
- package/src/summarized.js +15 -26
- package/src/summary.js +11 -19
- package/src/throttled.js +19 -24
- package/src/tokens.js +23 -25
- package/src/tty.js +79 -87
- package/src/wand.js +26 -29
- package/src/wheel.js +5 -9
- package/src/wish.js +10 -23
- package/src/wishes.js +25 -31
package/bin/jinka.js
CHANGED
|
@@ -7,31 +7,34 @@ import { readFile } from 'node:fs/promises';
|
|
|
7
7
|
import { resolve, join, dirname, parse } from 'node:path';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
10
|
+
import { jinka } from '../src/jinka.js';
|
|
11
|
+
import { wishes } from '../src/wishes.js';
|
|
12
|
+
import { agent } from '../src/agent.js';
|
|
13
|
+
import { claude } from '../src/claude.js';
|
|
14
|
+
import { codex } from '../src/codex.js';
|
|
15
|
+
import { genie } from '../src/genie.js';
|
|
16
|
+
import { disk } from '../src/disk.js';
|
|
17
|
+
import { tty } from '../src/tty.js';
|
|
18
|
+
import { colorless } from '../src/colorless.js';
|
|
19
|
+
import { log } from '../src/log.js';
|
|
20
|
+
import { status } from '../src/status.js';
|
|
21
|
+
import { quota } from '../src/quota.js';
|
|
22
|
+
import { rollouts } from '../src/rollouts.js';
|
|
23
|
+
import { meters } from '../src/meters.js';
|
|
24
|
+
import { freeze } from '../src/freeze.js';
|
|
25
|
+
import { frozen } from '../src/frozen.js';
|
|
26
|
+
import { limited } from '../src/limited.js';
|
|
27
|
+
import { throttled } from '../src/throttled.js';
|
|
28
|
+
import { keys } from '../src/keys.js';
|
|
29
|
+
import { wheel } from '../src/wheel.js';
|
|
30
|
+
import { legend } from '../src/legend.js';
|
|
31
|
+
import { release } from '../src/release.js';
|
|
32
|
+
import { tokens } from '../src/tokens.js';
|
|
33
|
+
import { crew } from '../src/crew.js';
|
|
34
|
+
import { jobs } from '../src/jobs.js';
|
|
35
|
+
import { summary } from '../src/summary.js';
|
|
36
|
+
import { summarized } from '../src/summarized.js';
|
|
37
|
+
import { semaphore } from '../src/semaphore.js';
|
|
35
38
|
|
|
36
39
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
40
|
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
@@ -61,9 +64,10 @@ const program = new Command()
|
|
|
61
64
|
.name('jinka')
|
|
62
65
|
.description(`${pkg.description} (${pkg.version})`)
|
|
63
66
|
.version(pkg.version)
|
|
67
|
+
.configureHelp({ sortOptions: true })
|
|
64
68
|
.showHelpAfterError('(add --help for additional information)')
|
|
65
69
|
.addOption(new Option('--wishes <dir>', 'directory with wishes, one per Markdown file').default(join(homedir(), '.jinka', 'wishes'), '~/.jinka/wishes'))
|
|
66
|
-
.option('--threads <n>', `how many wishes run at once, up to ${limit}, which the + and - keys change while running`, bounded,
|
|
70
|
+
.option('--threads <n>', `how many wishes run at once, up to ${limit}, which the + and - keys change while running`, bounded, 6)
|
|
67
71
|
.addOption(new Option('--archive <dir>', 'directory where finished wishes are moved instead of being deleted').default(join(homedir(), '.jinka', 'archive'), '~/.jinka/archive'))
|
|
68
72
|
.addOption(new Option('--logs <dir>', 'directory where the output of every agent run is saved').default(join(homedir(), '.jinka', 'logs'), '~/.jinka/logs'))
|
|
69
73
|
.addOption(new Option('--memory <dir>', 'directory where agents keep what they learn about repositories between runs').default(join(homedir(), '.jinka', 'memory'), '~/.jinka/memory'))
|
|
@@ -75,31 +79,45 @@ const program = new Command()
|
|
|
75
79
|
.addOption(new Option('--model <name>', 'model passed to the agent').default('', 'agent default'))
|
|
76
80
|
.addOption(new Option('--effort <level>', 'reasoning effort passed to the agent, such as low, medium, high, xhigh, or max').default('', 'agent default'))
|
|
77
81
|
.option('--throttle <percent>', 'how much of the agent quota, in percent, must be left in every window before a new job starts', percent, 15)
|
|
82
|
+
.addOption(new Option('--pid <file>', 'file that exists while jinka runs, so that a second jinka refuses to start').default(join(homedir(), '.jinka', 'pid.txt'), '~/.jinka/pid.txt'))
|
|
83
|
+
.option('--no-color', 'paint nothing in the terminal, which a non-empty NO_COLOR environment variable also does')
|
|
84
|
+
.option('--no-tui', 'print only the protocol, without the status bars and the list of threads')
|
|
78
85
|
.addOption(new Option('--limit <n>', 'how many jobs start before jinka waits for them to finish and quits').argParser(positive).default(Infinity, 'unlimited'))
|
|
79
86
|
.parse();
|
|
80
87
|
const values = program.opts();
|
|
88
|
+
const s = semaphore(resolve(values.pid));
|
|
89
|
+
await s.acquire().catch((e) => {
|
|
90
|
+
console.error(e.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
process.on('exit', () => s.release());
|
|
81
94
|
const dir = resolve(values.wishes);
|
|
82
95
|
const engine = values.engine ?? (parse(values.agent).name === 'codex' ? 'codex' : 'claude');
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
96
|
+
const a = values.agent === '' ? engine : values.agent;
|
|
97
|
+
const t = tokens();
|
|
98
|
+
const q = quota();
|
|
99
|
+
const c = crew(values.threads, limit, { warn: (text) => screen.notice().warn(text) });
|
|
100
|
+
const j = jobs(genie((engine === 'codex' ? codex : claude)(agent(a), values.model, values.effort, resolve(values.skill)), disk(dir, resolve(values.logs), resolve(values.memory), c), meters([t, engine === 'codex' ? rollouts(q, join(process.env.CODEX_HOME ?? join(homedir(), '.codex'), 'sessions')) : q])));
|
|
101
|
+
const f = freeze(c, { warn: (text) => screen.notice().warn(text) }, () => quit(0));
|
|
102
|
+
const r = release(pkg.version, 'https://registry.npmjs.org/@yegor256/jinka/latest');
|
|
89
103
|
const start = Date.now();
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
const tui = process.stdout.isTTY && values.tui;
|
|
105
|
+
const screen = summarized(
|
|
106
|
+
tui
|
|
107
|
+
? tty(
|
|
108
|
+
values.color && !process.env.NO_COLOR ? process.stdout : colorless(process.stdout),
|
|
109
|
+
c,
|
|
110
|
+
status(start, j, a, values.model, t, q),
|
|
111
|
+
legend(f, r)
|
|
97
112
|
)
|
|
98
|
-
:
|
|
99
|
-
|
|
113
|
+
: log(process.stdout),
|
|
114
|
+
summary(start, pkg.version, j, { agent: a, model: values.model, effort: values.effort, wishes: dir }),
|
|
100
115
|
process.stdout
|
|
101
116
|
);
|
|
102
117
|
process.on('exit', () => screen.close());
|
|
118
|
+
if (tui) {
|
|
119
|
+
r.check();
|
|
120
|
+
}
|
|
103
121
|
const quit = (code) => {
|
|
104
122
|
screen.close();
|
|
105
123
|
process.stdout.write('', () => process.stderr.write('', () => process.exit(code)));
|
|
@@ -112,18 +130,18 @@ const interrupt = () => {
|
|
|
112
130
|
controller.abort();
|
|
113
131
|
};
|
|
114
132
|
process.on('SIGINT', interrupt);
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
keys(process.stdin, f, c, interrupt).listen();
|
|
134
|
+
wheel(process.stdin, screen).listen();
|
|
117
135
|
try {
|
|
118
|
-
await
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
await jinka(
|
|
137
|
+
limited(frozen(throttled(wishes(dir, values.interval, values.archive === '' ? '' : resolve(values.archive), values.pause, { warn: (text) => screen.notice().warn(text) }), q, values.throttle, 15 * 60 * 1000, { warn: (text) => screen.notice().warn(text) }), f), values.limit, f),
|
|
138
|
+
j,
|
|
121
139
|
screen,
|
|
122
|
-
|
|
140
|
+
c
|
|
123
141
|
).run(controller.signal);
|
|
124
142
|
quit(130);
|
|
125
143
|
} catch (e) {
|
|
126
144
|
screen.close();
|
|
127
|
-
console.error(e
|
|
145
|
+
console.error(e);
|
|
128
146
|
quit(1);
|
|
129
147
|
}
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import spawn from 'cross-spawn';
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
this.command = command;
|
|
9
|
-
}
|
|
10
|
-
start(args, signal) {
|
|
6
|
+
export const agent = (command) => Object.freeze({
|
|
7
|
+
start: (args, signal) => {
|
|
11
8
|
const windows = process.platform === 'win32';
|
|
12
|
-
const child = spawn(
|
|
9
|
+
const child = spawn(command, args, windows ? { stdio: 'pipe' } : { signal, stdio: 'pipe' });
|
|
13
10
|
if (windows) {
|
|
14
11
|
signal.addEventListener(
|
|
15
12
|
'abort',
|
|
@@ -19,4 +16,4 @@ export class Agent {
|
|
|
19
16
|
}
|
|
20
17
|
return child;
|
|
21
18
|
}
|
|
22
|
-
}
|
|
19
|
+
});
|
package/src/archived.js
CHANGED
|
@@ -4,34 +4,26 @@
|
|
|
4
4
|
import { copyFile, mkdir, access } from 'node:fs/promises';
|
|
5
5
|
import { join, dirname, extname } from 'node:path';
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return this.origin.name();
|
|
15
|
-
}
|
|
16
|
-
async text() {
|
|
17
|
-
return this.origin.text();
|
|
18
|
-
}
|
|
19
|
-
fate() {
|
|
20
|
-
return this.origin.fate();
|
|
21
|
-
}
|
|
22
|
-
async close() {
|
|
23
|
-
await copyFile(join(this.dir, this.origin.name()), await this.#target());
|
|
24
|
-
await this.origin.close();
|
|
25
|
-
}
|
|
26
|
-
free() {
|
|
27
|
-
this.origin.free();
|
|
28
|
-
}
|
|
29
|
-
async #target() {
|
|
30
|
-
const target = join(this.archive, this.origin.name());
|
|
31
|
-
await mkdir(dirname(target), { recursive: true });
|
|
32
|
-
return access(target).then(
|
|
33
|
-
() => `${target.slice(0, target.length - extname(target).length)}-${Date.now()}${extname(target)}`,
|
|
34
|
-
() => target
|
|
7
|
+
export const archived = (origin, dir, archive) => {
|
|
8
|
+
const target = async () => {
|
|
9
|
+
const path = join(archive, origin.name());
|
|
10
|
+
await mkdir(dirname(path), { recursive: true });
|
|
11
|
+
return access(path).then(
|
|
12
|
+
() => `${path.slice(0, path.length - extname(path).length)}-${Date.now()}${extname(path)}`,
|
|
13
|
+
() => path
|
|
35
14
|
);
|
|
36
|
-
}
|
|
37
|
-
|
|
15
|
+
};
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
name: () => origin.name(),
|
|
18
|
+
text: async () => origin.text(),
|
|
19
|
+
fate: () => origin.fate(),
|
|
20
|
+
close: async () => {
|
|
21
|
+
await access(join(dir, origin.name())).then(
|
|
22
|
+
async () => copyFile(join(dir, origin.name()), await target()),
|
|
23
|
+
() => {}
|
|
24
|
+
);
|
|
25
|
+
await origin.close();
|
|
26
|
+
},
|
|
27
|
+
free: () => origin.free()
|
|
28
|
+
});
|
|
29
|
+
};
|
package/src/claude.js
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...(this.model === '' ? [] : ['--model', this.model]),
|
|
19
|
-
...(this.effort === '' ? [] : ['--effort', this.effort])
|
|
20
|
-
],
|
|
21
|
-
signal
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
lines() {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
4
|
+
export const claude = (agent, model, effort, plugin) => Object.freeze({
|
|
5
|
+
start: (args, signal) => agent.start(
|
|
6
|
+
[
|
|
7
|
+
'-p', '--output-format', 'stream-json', '--verbose',
|
|
8
|
+
'--plugin-dir', plugin,
|
|
9
|
+
...args,
|
|
10
|
+
'--dangerously-skip-permissions',
|
|
11
|
+
...(model === '' ? [] : ['--model', model]),
|
|
12
|
+
...(effort === '' ? [] : ['--effort', effort])
|
|
13
|
+
],
|
|
14
|
+
signal
|
|
15
|
+
),
|
|
16
|
+
lines: () => []
|
|
17
|
+
});
|
package/src/codex.js
CHANGED
|
@@ -3,30 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
signal
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
lines() {
|
|
27
|
-
return [
|
|
28
|
-
`Your skills live in the directory ${join(this.plugin, 'skills')}, one skill per subdirectory, each described in its SKILL.md file.`,
|
|
29
|
-
'Before you start, read the descriptions of these skills, and whenever a skill fits your work, read its SKILL.md file and follow it.'
|
|
30
|
-
];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
6
|
+
export const codex = (agent, model, effort, plugin) => Object.freeze({
|
|
7
|
+
start: (args, signal) => agent.start(
|
|
8
|
+
[
|
|
9
|
+
'exec', '--json',
|
|
10
|
+
'--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check',
|
|
11
|
+
...args,
|
|
12
|
+
...(model === '' ? [] : ['--model', model]),
|
|
13
|
+
...(effort === '' ? [] : ['--config', `model_reasoning_effort=${effort}`]),
|
|
14
|
+
'-'
|
|
15
|
+
],
|
|
16
|
+
signal
|
|
17
|
+
),
|
|
18
|
+
lines: () => [
|
|
19
|
+
`Your skills live in the directory ${join(plugin, 'skills')}, one skill per subdirectory, each described in its SKILL.md file.`,
|
|
20
|
+
'Before you start, read the descriptions of these skills, and whenever a skill fits your work, read its SKILL.md file and follow it.'
|
|
21
|
+
]
|
|
22
|
+
});
|
package/src/colorless.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
export const colorless = (stream) => Object.freeze({
|
|
5
|
+
get columns() {
|
|
6
|
+
return stream.columns;
|
|
7
|
+
},
|
|
8
|
+
get rows() {
|
|
9
|
+
return stream.rows;
|
|
10
|
+
},
|
|
11
|
+
on: (event, listener) => stream.on(event, listener),
|
|
12
|
+
write: (text, done) => stream.write(text.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''), done)
|
|
13
|
+
});
|
package/src/crew.js
CHANGED
|
@@ -1,67 +1,62 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
indexes() {
|
|
15
|
-
return [...this.seats.keys()].sort((a, b) => a - b);
|
|
16
|
-
}
|
|
17
|
-
more() {
|
|
18
|
-
if (this.seats.size >= this.limit) {
|
|
19
|
-
this.notice.warn(`more than ${this.limit} threads are not allowed`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const index = Array.from({ length: this.limit }, (_, i) => i).find((i) => !this.seats.has(i));
|
|
23
|
-
this.seats.set(index, { idle: false, controller: new AbortController() });
|
|
24
|
-
this.state.launch(index);
|
|
25
|
-
}
|
|
26
|
-
less() {
|
|
27
|
-
const index = this.indexes().findLast((i) => this.seats.get(i).idle);
|
|
28
|
-
if (this.seats.size === 1) {
|
|
29
|
-
this.notice.warn('the last thread cannot be removed');
|
|
30
|
-
} else if (index === undefined) {
|
|
31
|
-
this.notice.warn('no idle thread can be removed right now');
|
|
32
|
-
} else {
|
|
33
|
-
this.seats.get(index).controller.abort();
|
|
34
|
-
this.seats.delete(index);
|
|
4
|
+
export const crew = (size, limit, notice) => {
|
|
5
|
+
const seats = new Map(Array.from({ length: size }, (_, i) => [i, { idle: false, controller: new AbortController() }]));
|
|
6
|
+
const state = { launch: () => {}, runs: [] };
|
|
7
|
+
const indexes = () => [...seats.keys()].sort((a, b) => a - b);
|
|
8
|
+
const join = async (from) => {
|
|
9
|
+
const to = state.runs.length;
|
|
10
|
+
if (from < to) {
|
|
11
|
+
await Promise.all(state.runs.slice(from, to));
|
|
12
|
+
await join(to);
|
|
35
13
|
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
14
|
+
};
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
size: () => seats.size,
|
|
17
|
+
indexes,
|
|
18
|
+
more: () => {
|
|
19
|
+
if (seats.size >= limit) {
|
|
20
|
+
notice.warn(`more than ${limit} threads are not allowed`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const index = Array.from({ length: limit }, (_, i) => i).find((i) => !seats.has(i));
|
|
24
|
+
seats.set(index, { idle: false, controller: new AbortController() });
|
|
25
|
+
state.launch(index);
|
|
26
|
+
},
|
|
27
|
+
less: () => {
|
|
28
|
+
const index = indexes().findLast((i) => seats.get(i).idle);
|
|
29
|
+
if (seats.size === 1) {
|
|
30
|
+
notice.warn('the last thread cannot be removed');
|
|
31
|
+
} else if (index === undefined) {
|
|
32
|
+
notice.warn('no idle thread can be removed right now');
|
|
33
|
+
} else {
|
|
34
|
+
seats.get(index).controller.abort();
|
|
35
|
+
seats.delete(index);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
run: async (wishes, wand, signal) => {
|
|
39
|
+
state.launch = (index) => {
|
|
40
|
+
const seat = seats.get(index);
|
|
41
|
+
state.runs.push(
|
|
42
|
+
wand(
|
|
43
|
+
index,
|
|
44
|
+
{
|
|
45
|
+
next: async (s) => {
|
|
46
|
+
seat.idle = true;
|
|
47
|
+
try {
|
|
48
|
+
return await wishes.next(s);
|
|
49
|
+
} finally {
|
|
50
|
+
seat.idle = false;
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await this.#join(0);
|
|
59
|
-
}
|
|
60
|
-
async #join(from) {
|
|
61
|
-
const to = this.state.runs.length;
|
|
62
|
-
if (from < to) {
|
|
63
|
-
await Promise.all(this.state.runs.slice(from, to));
|
|
64
|
-
await this.#join(to);
|
|
53
|
+
},
|
|
54
|
+
AbortSignal.any([signal, seat.controller.signal])
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
indexes().forEach((i) => state.launch(i));
|
|
59
|
+
await join(0);
|
|
65
60
|
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
61
|
+
});
|
|
62
|
+
};
|
package/src/disk.js
CHANGED
|
@@ -5,37 +5,29 @@ import { createWriteStream } from 'node:fs';
|
|
|
5
5
|
import { mkdir } from 'node:fs/promises';
|
|
6
6
|
import { join, dirname, extname } from 'node:path';
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this
|
|
12
|
-
|
|
13
|
-
this
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`if something must happen first, write a new wish that waits for it and finishes the job, and then complete the current wish right away.`, `Your memory lives in the directory ${this.memory}, keep there what the skills tell you to keep.`,
|
|
26
|
-
`Jinka runs ${this.crew.size()} threads right now, so up to ${this.crew.size()} wishes run at once.`
|
|
27
|
-
];
|
|
28
|
-
}
|
|
29
|
-
args() {
|
|
30
|
-
return ['--add-dir', this.wishes, '--add-dir', this.memory];
|
|
31
|
-
}
|
|
32
|
-
async log(wish) {
|
|
8
|
+
export const disk = (wishes, logs, memory, crew) => Object.freeze({
|
|
9
|
+
lines: (wish) => [
|
|
10
|
+
`The wish is written in the file ${join(wishes, wish.name())}, ${wish.fate()}.`,
|
|
11
|
+
'Never delete, move, or rename the file of this wish yourself, even after you split it into smaller wishes, since Jinka takes care of it.',
|
|
12
|
+
`All wishes live in the directory ${wishes}, and you may add new wishes there as Markdown files.`,
|
|
13
|
+
`If this wish is an atomic job that involves no other jobs, make it come true yourself;`,
|
|
14
|
+
`otherwise, do not do it yourself, but split it into smaller wishes`,
|
|
15
|
+
`and write each of them as a separate Markdown file in the directory ${join(wishes, dirname(wish.name()))}.`,
|
|
16
|
+
`Always start the name of every new wish file with a small English letter, never with a capital one.`,
|
|
17
|
+
`In every Git repository you touch, do all your work inside a separate Git worktree, never in the main directory of the repository, so that other wishes running at the same time do not see your changes.`,
|
|
18
|
+
`Never spend time waiting while doing a wish, neither sleeping nor polling for a build, a review, a merge, or anything else;`,
|
|
19
|
+
`if something must happen first, write a new wish that waits for it and finishes the job, and then complete the current wish right away.`,
|
|
20
|
+
`Your memory lives in the directory ${memory}, keep there what the skills tell you to keep.`,
|
|
21
|
+
`Jinka runs ${crew.size()} threads right now, so up to ${crew.size()} wishes run at once.`
|
|
22
|
+
],
|
|
23
|
+
args: () => ['--add-dir', wishes, '--add-dir', memory],
|
|
24
|
+
log: async (wish) => {
|
|
33
25
|
const file = join(
|
|
34
|
-
|
|
26
|
+
logs,
|
|
35
27
|
`${wish.name().slice(0, wish.name().length - extname(wish.name()).length)}-${new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15)}.jsonl`
|
|
36
28
|
);
|
|
37
29
|
await mkdir(dirname(file), { recursive: true });
|
|
38
|
-
await mkdir(
|
|
30
|
+
await mkdir(memory, { recursive: true });
|
|
39
31
|
return createWriteStream(file);
|
|
40
32
|
}
|
|
41
|
-
}
|
|
33
|
+
});
|
package/src/duration.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
minutes() {
|
|
9
|
-
const seconds = Math.floor(this.millis / 1000);
|
|
4
|
+
export const duration = (millis) => {
|
|
5
|
+
const minutes = () => {
|
|
6
|
+
const seconds = Math.floor(millis / 1000);
|
|
10
7
|
return [Math.floor(seconds / 60), seconds % 60].map((p) => String(p).padStart(2, '0')).join(':');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
};
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
minutes,
|
|
11
|
+
clock: () => {
|
|
12
|
+
const total = Math.floor(millis / 60_000);
|
|
13
|
+
if (total < 1000) {
|
|
14
|
+
return minutes().padStart(6);
|
|
15
|
+
}
|
|
16
|
+
if (total < 6000) {
|
|
17
|
+
return `${String(Math.floor(total / 60)).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}m`;
|
|
18
|
+
}
|
|
19
|
+
return `${Math.floor(total / 60)}h`.padStart(6);
|
|
20
|
+
},
|
|
21
|
+
phrase: () => {
|
|
22
|
+
const seconds = Math.floor(millis / 1000);
|
|
23
|
+
const [count, unit] = seconds < 60 ? [seconds, 'second'] : [Math.floor(seconds / 60), 'minute'];
|
|
24
|
+
return `${count} ${unit}${count === 1 ? '' : 's'}`;
|
|
25
|
+
},
|
|
26
|
+
hours: () => {
|
|
27
|
+
const seconds = Math.floor(millis / 1000);
|
|
28
|
+
return [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60].map((p) => String(p).padStart(2, '0')).join(':');
|
|
16
29
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return `${Math.floor(minutes / 60)}h`.padStart(6);
|
|
21
|
-
}
|
|
22
|
-
phrase() {
|
|
23
|
-
const seconds = Math.floor(this.millis / 1000);
|
|
24
|
-
const [count, unit] = seconds < 60 ? [seconds, 'second'] : [Math.floor(seconds / 60), 'minute'];
|
|
25
|
-
return `${count} ${unit}${count === 1 ? '' : 's'}`;
|
|
26
|
-
}
|
|
27
|
-
hours() {
|
|
28
|
-
const seconds = Math.floor(this.millis / 1000);
|
|
29
|
-
return [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60].map((p) => String(p).padStart(2, '0')).join(':');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
export const ellipsized = (origin, limit) => Object.freeze({
|
|
5
|
+
text: () => (
|
|
6
|
+
origin.length < limit
|
|
7
|
+
? origin
|
|
8
|
+
: `${origin.slice(0, Math.floor((limit - 2) / 2))}…${origin.slice(-Math.ceil((limit - 2) / 2))}`
|
|
9
|
+
)
|
|
10
|
+
});
|