aligndev 0.22.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +11 -16
- package/dist/json-shape.d.ts +21 -0
- package/dist/json-shape.js +115 -0
- package/dist/project/layout.js +11 -7
- package/dist/project/markers.js +10 -15
- package/package.json +1 -4
- package/templates/guide/code.md +3 -3
package/dist/config.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { type } from "arktype";
|
|
4
3
|
import { CODING_AGENTS, detectCodingAgents } from "./code/coding-agent.js";
|
|
5
4
|
import { errorMessage } from "./errors.js";
|
|
5
|
+
import { arrayOf, boolean, literal, nonEmptyString, object, optional, parseShape, string, } from "./json-shape.js";
|
|
6
6
|
export const PLATFORMS = ["openclaw", "codingAgent"];
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"unset?": "string[]",
|
|
7
|
+
const codeShape = object({
|
|
8
|
+
agent: optional(literal(...CODING_AGENTS)),
|
|
9
|
+
models: optional(arrayOf(string)),
|
|
10
|
+
skipPermissions: optional(boolean),
|
|
11
|
+
unset: optional(arrayOf(string)),
|
|
13
12
|
});
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"code?": codeSchema,
|
|
13
|
+
const configShape = object({
|
|
14
|
+
platform: optional(literal(...PLATFORMS)),
|
|
15
|
+
projectsRoot: optional(nonEmptyString),
|
|
16
|
+
code: optional(codeShape),
|
|
19
17
|
});
|
|
20
18
|
// An absent file is a normal state: every key takes its default.
|
|
21
19
|
export function loadConfig(home) {
|
|
@@ -44,10 +42,7 @@ function parseConfigFile(path) {
|
|
|
44
42
|
catch (error) {
|
|
45
43
|
throw invalidConfig(path, errorMessage(error));
|
|
46
44
|
}
|
|
47
|
-
|
|
48
|
-
if (config instanceof type.errors)
|
|
49
|
-
throw invalidConfig(path, config.summary.split("\n", 1)[0]);
|
|
50
|
-
return config;
|
|
45
|
+
return parseShape(configShape, value, (detail) => invalidConfig(path, detail));
|
|
51
46
|
}
|
|
52
47
|
function invalidConfig(path, detail) {
|
|
53
48
|
return new Error(`Error: invalid aligndev config ${path}: ${detail}`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Check<T> = (value: unknown, path: string) => T;
|
|
2
|
+
export interface OptionalField<T> {
|
|
3
|
+
optional: Check<T>;
|
|
4
|
+
}
|
|
5
|
+
type Fields<T> = {
|
|
6
|
+
[K in keyof T]-?: undefined extends T[K] ? OptionalField<Exclude<T[K], undefined>> : Check<T[K]>;
|
|
7
|
+
};
|
|
8
|
+
/** Runs `check` at the root; a shape failure becomes `toError(<message>)`. */
|
|
9
|
+
export declare function parseShape<T>(check: Check<T>, value: unknown, toError: (detail: string) => Error): T;
|
|
10
|
+
/** Rejects the first unknown key, then checks each field in declaration order. */
|
|
11
|
+
export declare function object<T extends object>(fields: Fields<T>): Check<T>;
|
|
12
|
+
export declare function optional<T>(check: Check<T>): OptionalField<T>;
|
|
13
|
+
export declare function literal<const T extends readonly (string | number)[]>(...values: T): Check<T[number]>;
|
|
14
|
+
export declare function integerBetween(min: number, max: number): Check<number>;
|
|
15
|
+
export declare function nonEmptyString(value: unknown, path: string): string;
|
|
16
|
+
export declare function string(value: unknown, path: string): string;
|
|
17
|
+
export declare function boolean(value: unknown, path: string): boolean;
|
|
18
|
+
export declare function matching(pattern: RegExp): Check<string>;
|
|
19
|
+
export declare function arrayOf<T>(check: Check<T>): Check<T[]>;
|
|
20
|
+
export declare function nonEmptyArrayOf<T>(check: Check<T>): Check<T[]>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
2
|
+
class ShapeError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Runs `check` at the root; a shape failure becomes `toError(<message>)`. */
|
|
5
|
+
export function parseShape(check, value, toError) {
|
|
6
|
+
try {
|
|
7
|
+
return check(value, "");
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error instanceof ShapeError)
|
|
11
|
+
throw toError(error.message);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Rejects the first unknown key, then checks each field in declaration order. */
|
|
16
|
+
export function object(fields) {
|
|
17
|
+
const entries = Object.entries(fields);
|
|
18
|
+
return (value, path) => {
|
|
19
|
+
if (!isPlainObject(value))
|
|
20
|
+
throw shapeError(path, "be an object");
|
|
21
|
+
const unknownKey = Object.keys(value).find((key) => !Object.hasOwn(fields, key));
|
|
22
|
+
if (unknownKey !== undefined)
|
|
23
|
+
throw shapeError(childPath(path, unknownKey), "be removed");
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const [key, field] of entries) {
|
|
26
|
+
const fieldPath = childPath(path, key);
|
|
27
|
+
if (typeof field === "function")
|
|
28
|
+
result[key] = field(value[key], fieldPath);
|
|
29
|
+
else if (value[key] !== undefined)
|
|
30
|
+
result[key] = field.optional(value[key], fieldPath);
|
|
31
|
+
}
|
|
32
|
+
// The fields mirror T key by key.
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
if (typeof value !== "object" || value === null)
|
|
38
|
+
return false;
|
|
39
|
+
const prototype = Object.getPrototypeOf(value);
|
|
40
|
+
return prototype === Object.prototype || prototype === null;
|
|
41
|
+
}
|
|
42
|
+
function childPath(path, key) {
|
|
43
|
+
if (!IDENTIFIER.test(key))
|
|
44
|
+
return bracketPath(path, JSON.stringify(key));
|
|
45
|
+
return path === "" ? key : `${path}.${key}`;
|
|
46
|
+
}
|
|
47
|
+
function bracketPath(path, segment) {
|
|
48
|
+
return `${path === "" ? "value" : path}[${segment}]`;
|
|
49
|
+
}
|
|
50
|
+
function shapeError(path, expectation) {
|
|
51
|
+
return new ShapeError(`${path === "" ? "value" : path} must ${expectation}`);
|
|
52
|
+
}
|
|
53
|
+
export function optional(check) {
|
|
54
|
+
return { optional: check };
|
|
55
|
+
}
|
|
56
|
+
export function literal(...values) {
|
|
57
|
+
const expectation = `be ${listOf(values.map((item) => JSON.stringify(item)))}`;
|
|
58
|
+
return (value, path) => {
|
|
59
|
+
const match = values.find((item) => item === value);
|
|
60
|
+
if (match === undefined)
|
|
61
|
+
throw shapeError(path, expectation);
|
|
62
|
+
return match;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function listOf(items) {
|
|
66
|
+
if (items.length === 1)
|
|
67
|
+
return items[0];
|
|
68
|
+
return `${items.slice(0, -1).join(", ")} or ${items.at(-1)}`;
|
|
69
|
+
}
|
|
70
|
+
export function integerBetween(min, max) {
|
|
71
|
+
return (value, path) => {
|
|
72
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max)
|
|
73
|
+
throw shapeError(path, `be an integer from ${min} to ${max}`);
|
|
74
|
+
return value;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function nonEmptyString(value, path) {
|
|
78
|
+
if (typeof value !== "string" || value.length === 0)
|
|
79
|
+
throw shapeError(path, "be a non-empty string");
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
export function string(value, path) {
|
|
83
|
+
if (typeof value !== "string")
|
|
84
|
+
throw shapeError(path, "be a string");
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
export function boolean(value, path) {
|
|
88
|
+
if (typeof value !== "boolean")
|
|
89
|
+
throw shapeError(path, "be a boolean");
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
export function matching(pattern) {
|
|
93
|
+
return (value, path) => {
|
|
94
|
+
const text = string(value, path);
|
|
95
|
+
if (!pattern.test(text))
|
|
96
|
+
throw shapeError(path, `match ${String(pattern)}`);
|
|
97
|
+
return text;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function arrayOf(check) {
|
|
101
|
+
return (value, path) => {
|
|
102
|
+
if (!Array.isArray(value))
|
|
103
|
+
throw shapeError(path, "be an array");
|
|
104
|
+
return value.map((item, index) => check(item, bracketPath(path, String(index))));
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function nonEmptyArrayOf(check) {
|
|
108
|
+
const checkArray = arrayOf(check);
|
|
109
|
+
return (value, path) => {
|
|
110
|
+
const items = checkArray(value, path);
|
|
111
|
+
if (items.length === 0)
|
|
112
|
+
throw shapeError(path, "be a non-empty array");
|
|
113
|
+
return items;
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/project/layout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runAlignfirst } from "../alignfirst-cli.js";
|
|
2
2
|
import { errorMessage } from "../errors.js";
|
|
3
|
-
const MIN_ALIGNFIRST_VERSION = "0.
|
|
3
|
+
const MIN_ALIGNFIRST_VERSION = "0.9.0";
|
|
4
4
|
export function readProjectReport(command, cwd, env) {
|
|
5
5
|
const result = runAlignfirst(command, ["config", "--json"], cwd, env);
|
|
6
6
|
if (result.status !== 0) {
|
|
@@ -14,16 +14,24 @@ export function readProjectReport(command, cwd, env) {
|
|
|
14
14
|
catch (error) {
|
|
15
15
|
throw new Error(`Invalid alignfirst config report for ${cwd}: ${errorMessage(error)}`);
|
|
16
16
|
}
|
|
17
|
+
if (isOutdatedReport(value))
|
|
18
|
+
return { error: outdatedAlignfirst(cwd) };
|
|
17
19
|
return parseProjectReport(value, cwd);
|
|
18
20
|
}
|
|
19
21
|
function firstLine(value) {
|
|
20
22
|
return value.trim().split("\n", 1)[0] ?? "";
|
|
21
23
|
}
|
|
24
|
+
// Before alignfirst 0.9.0, the report had no `.alignfirst-instructions` location.
|
|
25
|
+
function isOutdatedReport(value) {
|
|
26
|
+
return (isRecord(value) && !(isRecord(value.locations) && ".alignfirst-instructions" in value.locations));
|
|
27
|
+
}
|
|
28
|
+
function outdatedAlignfirst(path) {
|
|
29
|
+
return (`The alignfirst CLI used in ${path} is too old: aligndev requires alignfirst ` +
|
|
30
|
+
`${MIN_ALIGNFIRST_VERSION} or later.`);
|
|
31
|
+
}
|
|
22
32
|
function parseProjectReport(value, path) {
|
|
23
33
|
if (!isRecord(value))
|
|
24
34
|
throw invalidReport(path);
|
|
25
|
-
if (value.source === "root" || value.locations === undefined)
|
|
26
|
-
throw outdatedAlignfirst(path);
|
|
27
35
|
return {
|
|
28
36
|
source: parseSource(value.source, path),
|
|
29
37
|
cli: parseCli(value.cli, path),
|
|
@@ -109,10 +117,6 @@ function parseLocation(value, path) {
|
|
|
109
117
|
function invalidReport(path) {
|
|
110
118
|
return new Error(`Invalid alignfirst config report for ${path}`);
|
|
111
119
|
}
|
|
112
|
-
function outdatedAlignfirst(path) {
|
|
113
|
-
return new Error(`The alignfirst CLI used in ${path} is too old: aligndev requires alignfirst ` +
|
|
114
|
-
`${MIN_ALIGNFIRST_VERSION} or later.`);
|
|
115
|
-
}
|
|
116
120
|
function isPortRange(value) {
|
|
117
121
|
return isRecord(value) && typeof value.first === "number" && typeof value.last === "number";
|
|
118
122
|
}
|
package/dist/project/markers.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { type } from "arktype";
|
|
4
3
|
import { errorMessage } from "../errors.js";
|
|
4
|
+
import { integerBetween, matching, nonEmptyArrayOf, object, optional, parseShape, string, } from "../json-shape.js";
|
|
5
5
|
import { formatRange } from "./format.js";
|
|
6
6
|
export const MARKER_FILENAME = ".alignfirst-projects.json";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"description?": "string",
|
|
7
|
+
const markerPortRangeShape = object({
|
|
8
|
+
first: integerBetween(1, 65_535),
|
|
9
|
+
last: integerBetween(1, 65_535),
|
|
10
|
+
code: optional(matching(/^[a-z][a-z0-9-]*$/)),
|
|
11
|
+
description: optional(string),
|
|
13
12
|
});
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"portRanges?": markerPortRangeSchema.array().atLeastLength(1),
|
|
13
|
+
const markerShape = object({
|
|
14
|
+
description: optional(string),
|
|
15
|
+
portRanges: optional(nonEmptyArrayOf(markerPortRangeShape)),
|
|
18
16
|
});
|
|
19
17
|
export function readMarker(dir) {
|
|
20
18
|
const path = join(dir, MARKER_FILENAME);
|
|
@@ -27,10 +25,7 @@ export function readMarker(dir) {
|
|
|
27
25
|
catch (error) {
|
|
28
26
|
throw invalidMarker(path, errorMessage(error));
|
|
29
27
|
}
|
|
30
|
-
const marker =
|
|
31
|
-
if (marker instanceof type.errors) {
|
|
32
|
-
throw invalidMarker(path, marker.summary.split("\n", 1)[0]);
|
|
33
|
-
}
|
|
28
|
+
const marker = parseShape(markerShape, value, (detail) => invalidMarker(path, detail));
|
|
34
29
|
if (marker.portRanges !== undefined)
|
|
35
30
|
assertValidPortRanges(marker.portRanges, path);
|
|
36
31
|
return marker;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aligndev",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"license": "CC0-1.0",
|
|
5
5
|
"author": "Thomas MUR",
|
|
6
6
|
"description": "The AlignDev CLI: the assistant's playbook, coding-agent delegation, and project inventory.",
|
|
@@ -37,9 +37,6 @@
|
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"arktype": "^2.2.3"
|
|
42
|
-
},
|
|
43
40
|
"devDependencies": {
|
|
44
41
|
"@types/node": "~24.13.3",
|
|
45
42
|
"rimraf": "~6.1.3",
|
package/templates/guide/code.md
CHANGED
|
@@ -159,17 +159,17 @@ Run the chain end to end. The plan is a step of the implementation, not a checkp
|
|
|
159
159
|
|
|
160
160
|
### Where the plan runs
|
|
161
161
|
|
|
162
|
-
Planning in the spec's own session is cheaper: the agent already holds the investigation. That advantage ends once the session fills up, because the spec discussion competes with the planning work for the same context window. `contextTokens` in the `{{ALIGNDEV}} code status` output is the measure; the threshold is **
|
|
162
|
+
Planning in the spec's own session is cheaper: the agent already holds the investigation. That advantage ends once the session fills up, because the spec discussion competes with the planning work for the same context window. `contextTokens` in the `{{ALIGNDEV}} code status` output is the measure; the threshold is **160k**.
|
|
163
163
|
|
|
164
164
|
Read `contextCompacted` first. When it is `true`, the agent compacted the conversation: the investigation now survives only as a summary, so the advantage of staying is already gone. Plan in a fresh session whatever `contextTokens` says. Treat an empty `contextTokens` the same way — `contextTokensError` says why the figure is missing, and an unknown occupancy is not a reason to gamble on staying.
|
|
165
165
|
|
|
166
|
-
**Below
|
|
166
|
+
**Below 160k — plan in the spec session.** Send the protocol with no message:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
{{ALIGNDEV}} code resume <sessionId> --protocol plan
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
**At or above
|
|
172
|
+
**At or above 160k — make the spec stand alone, then plan in a fresh session.** The next session reads the spec file and nothing else, so the spec must carry every decision the discussion settled. Ask for that first, in the session that holds the discussion:
|
|
173
173
|
|
|
174
174
|
```bash
|
|
175
175
|
{{ALIGNDEV}} code resume <sessionId> --message "Ensure this spec is self-sufficient: another session will write the plans from it."
|