@aiwg/cli 2026.7.24 → 2026.8.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/dist/src/artifacts/cli.js +53 -10
- package/dist/src/artifacts/fortemi-shard-export.js +107 -18
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/job.js +97 -0
- package/dist/src/cli/handlers/runtime-info.js +2 -2
- package/dist/src/cli/handlers/serve.js +2 -2
- package/dist/src/cli/handlers/sessions.js +188 -0
- package/dist/src/cli/handlers/steward.js +16 -3
- package/dist/src/extensions/commands/definitions.js +30 -5
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/features/catalog.js +3 -3
- package/dist/src/jobs/executor.js +83 -0
- package/dist/src/jobs/flow.js +106 -0
- package/dist/src/jobs/gitea.js +91 -0
- package/dist/src/jobs/render.js +53 -0
- package/dist/src/jobs/runner.js +315 -0
- package/dist/src/jobs/types.js +3 -0
- package/dist/src/providers/capability-matrix.js +11 -4
- package/dist/src/providers/capability-matrix.yaml +39 -42
- package/dist/src/sessions/analytics.js +303 -0
- package/dist/src/sessions/importer.js +7 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/policy.js +1 -1
- package/dist/src/sessions/repository.js +213 -0
- package/package.json +10 -10
|
@@ -937,12 +937,19 @@ async function handleExport(args) {
|
|
|
937
937
|
console.log(' --out <path> Write JSON or .shard output to a file');
|
|
938
938
|
console.log(' --repo <name> Source repository label (default: cwd basename)');
|
|
939
939
|
console.log(' --privacy <level> private, sanitized, or public (default: private)');
|
|
940
|
-
console.log(' --schema-version <v>
|
|
940
|
+
console.log(' --schema-version <v> Browser: v1|v2; shard: 2.0.0|1.2.0');
|
|
941
|
+
console.log(' --profile <name> Shard profile: full-v1 (default) or core-v1');
|
|
942
|
+
console.log(' --fail-on-loss Reject a full-v1 conversion that reports any loss');
|
|
943
|
+
console.log(' --dry-run Build and report without writing the shard');
|
|
944
|
+
console.log(' --force Replace an existing output path');
|
|
945
|
+
console.log(' --migrate-from <path> Diagnose a source-less legacy shard (dry-run only)');
|
|
946
|
+
console.log(' --json Emit the machine-readable conversion report');
|
|
941
947
|
console.log(' --generated-at <iso> Override generated timestamp for deterministic fixtures');
|
|
942
948
|
console.log('');
|
|
943
949
|
console.log('Examples:');
|
|
944
950
|
console.log(' aiwg index export --format fortemi --graph project --out aiwg-fortemi-index.json');
|
|
945
|
-
console.log(' aiwg index export --format fortemi-shard --graph project --out aiwg-index.shard');
|
|
951
|
+
console.log(' aiwg index export --format fortemi-shard --graph project --schema-version 2.0.0 --profile full-v1 --fail-on-loss --out aiwg-index.shard');
|
|
952
|
+
console.log(' aiwg index export --format fortemi-shard --migrate-from legacy.shard --dry-run --json');
|
|
946
953
|
console.log(' aiwg index export --format fortemi --privacy sanitized --generated-at 2026-01-01T00:00:00.000Z');
|
|
947
954
|
return;
|
|
948
955
|
}
|
|
@@ -960,29 +967,65 @@ async function handleExport(args) {
|
|
|
960
967
|
process.exit(1);
|
|
961
968
|
}
|
|
962
969
|
const generatedAt = parseFlagValue(args, '--generated-at', 'Error: --generated-at requires an ISO timestamp value');
|
|
963
|
-
const schemaVersion = parseFlagValue(args, '--schema-version', 'Error: --schema-version
|
|
964
|
-
|
|
965
|
-
|
|
970
|
+
const schemaVersion = parseFlagValue(args, '--schema-version', 'Error: --schema-version requires a value');
|
|
971
|
+
const profile = parseFlagValue(args, '--profile', 'Error: --profile requires a value');
|
|
972
|
+
const migrateFrom = parseFlagValue(args, '--migrate-from', 'Error: --migrate-from requires a shard path');
|
|
973
|
+
const dryRun = args.includes('--dry-run');
|
|
974
|
+
const json = args.includes('--json');
|
|
975
|
+
if (format === 'fortemi' && schemaVersion && !['v1', 'v2'].includes(schemaVersion)) {
|
|
976
|
+
console.error('Error: browser export --schema-version must be v1 or v2');
|
|
977
|
+
process.exit(1);
|
|
978
|
+
}
|
|
979
|
+
if (format === 'fortemi-shard' && schemaVersion && !['1.2.0', '2.0.0'].includes(schemaVersion)) {
|
|
980
|
+
console.error('Error: shard --schema-version must be 2.0.0 or 1.2.0');
|
|
966
981
|
process.exit(1);
|
|
967
982
|
}
|
|
968
|
-
if (format === 'fortemi-shard' &&
|
|
969
|
-
console.error('Error: --
|
|
983
|
+
if (format === 'fortemi-shard' && profile && !['full-v1', 'core-v1'].includes(profile)) {
|
|
984
|
+
console.error('Error: shard --profile must be full-v1 or core-v1');
|
|
970
985
|
process.exit(1);
|
|
971
986
|
}
|
|
972
|
-
if (format === 'fortemi-shard' && !out) {
|
|
987
|
+
if (format === 'fortemi-shard' && !out && !migrateFrom) {
|
|
973
988
|
console.error('Error: --format fortemi-shard requires --out <path>');
|
|
974
989
|
process.exit(1);
|
|
975
990
|
}
|
|
976
991
|
try {
|
|
977
992
|
if (format === 'fortemi-shard') {
|
|
978
|
-
const { writeAiwgFortemiKnowledgeShard } = await import('./fortemi-shard-export.js');
|
|
993
|
+
const { diagnoseAiwgFortemiShardMigration, writeAiwgFortemiKnowledgeShard, } = await import('./fortemi-shard-export.js');
|
|
994
|
+
if (migrateFrom) {
|
|
995
|
+
if (!dryRun) {
|
|
996
|
+
throw new Error('--migrate-from is diagnostic-only and requires --dry-run; '
|
|
997
|
+
+ 'source-less core-v1 artifacts cannot be promoted losslessly.');
|
|
998
|
+
}
|
|
999
|
+
const diagnosis = diagnoseAiwgFortemiShardMigration(process.cwd(), migrateFrom);
|
|
1000
|
+
if (json)
|
|
1001
|
+
console.log(JSON.stringify(diagnosis, null, 2));
|
|
1002
|
+
else {
|
|
1003
|
+
console.log(`Migration supported: no`);
|
|
1004
|
+
console.log(`Diagnostic: ${diagnosis.diagnostic}`);
|
|
1005
|
+
console.log(`Action: ${diagnosis.action}`);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
979
1009
|
const result = await writeAiwgFortemiKnowledgeShard(process.cwd(), out, {
|
|
980
1010
|
graph,
|
|
981
1011
|
repo,
|
|
982
1012
|
privacy: privacy,
|
|
983
1013
|
generatedAt,
|
|
1014
|
+
schemaVersion: schemaVersion,
|
|
1015
|
+
profile: profile,
|
|
1016
|
+
failOnLoss: args.includes('--fail-on-loss'),
|
|
1017
|
+
dryRun,
|
|
1018
|
+
overwrite: args.includes('--force'),
|
|
984
1019
|
});
|
|
985
|
-
|
|
1020
|
+
if (json)
|
|
1021
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1022
|
+
else {
|
|
1023
|
+
const action = result.written ? 'Exported' : 'Would export';
|
|
1024
|
+
console.log(`${action} ${result.items} AIWG records as `
|
|
1025
|
+
+ `${result.conversion.schemaVersion}/${result.conversion.profile} `
|
|
1026
|
+
+ `to ${result.outPath} (${result.bytes} bytes; `
|
|
1027
|
+
+ `${result.conversion.losses.length} losses)`);
|
|
1028
|
+
}
|
|
986
1029
|
return;
|
|
987
1030
|
}
|
|
988
1031
|
const { buildAiwgFortemiIndexExport, writeAiwgFortemiIndexExport } = await import('./browser-export.js');
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
|
|
4
|
-
|
|
4
|
+
const SUPPORTED_SHARD_TUPLES = new Set([
|
|
5
|
+
"1.2.0/core-v1",
|
|
6
|
+
"2.0.0/full-v1",
|
|
7
|
+
]);
|
|
8
|
+
export function resolveAiwgFortemiShardTuple(options) {
|
|
9
|
+
const schemaVersion = options.schemaVersion ?? "2.0.0";
|
|
10
|
+
const profile = options.profile ?? "full-v1";
|
|
11
|
+
if (!SUPPORTED_SHARD_TUPLES.has(`${schemaVersion}/${profile}`)) {
|
|
12
|
+
throw new Error(`Unsupported Fortemi Knowledge Shard tuple ${schemaVersion}/${profile}; `
|
|
13
|
+
+ "supported tuples are 2.0.0/full-v1 and 1.2.0/core-v1.");
|
|
14
|
+
}
|
|
15
|
+
return { schemaVersion, profile };
|
|
16
|
+
}
|
|
17
|
+
async function loadFortemiShardConverters() {
|
|
5
18
|
let core;
|
|
6
19
|
try {
|
|
7
20
|
core = (await import(
|
|
@@ -10,35 +23,111 @@ async function loadFortemiShardConverter() {
|
|
|
10
23
|
catch {
|
|
11
24
|
throw new Error("Portable Fortemi shard export requires @fortemi/core with aiwgFortemiIndexToKnowledgeShard.");
|
|
12
25
|
}
|
|
13
|
-
if (!core.aiwgFortemiIndexToKnowledgeShard
|
|
14
|
-
|
|
26
|
+
if (!core.aiwgFortemiIndexToKnowledgeShard
|
|
27
|
+
|| !core.aiwgFortemiIndexToKnowledgeShardWithReport) {
|
|
28
|
+
throw new Error("Installed @fortemi/core does not support both public AIWG shard converters; "
|
|
29
|
+
+ "upgrade to a compatible published release.");
|
|
15
30
|
}
|
|
16
|
-
return core
|
|
31
|
+
return core;
|
|
17
32
|
}
|
|
18
|
-
export async function
|
|
33
|
+
export async function buildAiwgFortemiKnowledgeShardWithReport(cwd, options = {}, converter, reportConverter) {
|
|
19
34
|
const exported = buildAiwgFortemiIndexExport(cwd, {
|
|
20
35
|
...options,
|
|
21
36
|
schemaVersion: "v2",
|
|
22
37
|
});
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const tuple = resolveAiwgFortemiShardTuple(options);
|
|
39
|
+
const needsCore = tuple.profile === "core-v1" ? !converter : !reportConverter;
|
|
40
|
+
const core = needsCore ? await loadFortemiShardConverters() : null;
|
|
41
|
+
if (tuple.profile === "core-v1") {
|
|
42
|
+
const archive = await (converter ?? core.aiwgFortemiIndexToKnowledgeShard)(exported, { createdAt: options.generatedAt, matricVersion: "aiwg" });
|
|
43
|
+
return {
|
|
44
|
+
...tuple,
|
|
45
|
+
success: true,
|
|
46
|
+
lossless: true,
|
|
47
|
+
losses: [],
|
|
48
|
+
receipt: null,
|
|
49
|
+
archive,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const result = await (reportConverter ?? core.aiwgFortemiIndexToKnowledgeShardWithReport)(exported, { createdAt: options.generatedAt, matricVersion: "aiwg" });
|
|
53
|
+
if (!result.success || !result.archive) {
|
|
54
|
+
throw new Error("Fortemi full-v1 conversion failed without producing an archive; losses="
|
|
55
|
+
+ JSON.stringify(result.losses));
|
|
56
|
+
}
|
|
57
|
+
if (options.failOnLoss && (!result.lossless || result.losses.length > 0)) {
|
|
58
|
+
throw new Error(`Fortemi full-v1 conversion reported ${result.losses.length} loss(es); `
|
|
59
|
+
+ "archive was not written because --fail-on-loss was set.");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...tuple,
|
|
63
|
+
success: result.success,
|
|
64
|
+
lossless: result.lossless,
|
|
65
|
+
losses: result.losses,
|
|
66
|
+
receipt: result.receipt,
|
|
67
|
+
archive: result.archive,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function buildAiwgFortemiKnowledgeShard(cwd, options = {}, converter, reportConverter) {
|
|
71
|
+
return (await buildAiwgFortemiKnowledgeShardWithReport(cwd, options, converter, reportConverter)).archive;
|
|
28
72
|
}
|
|
29
|
-
export async function writeAiwgFortemiKnowledgeShard(cwd, outPath, options = {}, converter) {
|
|
73
|
+
export async function writeAiwgFortemiKnowledgeShard(cwd, outPath, options = {}, converter, reportConverter) {
|
|
30
74
|
const exported = buildAiwgFortemiIndexExport(cwd, {
|
|
31
75
|
...options,
|
|
32
76
|
schemaVersion: "v2",
|
|
33
77
|
});
|
|
34
|
-
const
|
|
35
|
-
const shard = await convert(exported, {
|
|
36
|
-
createdAt: options.generatedAt,
|
|
37
|
-
matricVersion: "aiwg",
|
|
38
|
-
});
|
|
78
|
+
const conversion = await buildAiwgFortemiKnowledgeShardWithReport(cwd, options, converter, reportConverter);
|
|
39
79
|
const resolved = path.resolve(cwd, outPath);
|
|
80
|
+
const report = {
|
|
81
|
+
bytes: conversion.archive.byteLength,
|
|
82
|
+
items: exported.items.length,
|
|
83
|
+
outPath: resolved,
|
|
84
|
+
written: !options.dryRun,
|
|
85
|
+
conversion: {
|
|
86
|
+
schemaVersion: conversion.schemaVersion,
|
|
87
|
+
profile: conversion.profile,
|
|
88
|
+
success: conversion.success,
|
|
89
|
+
lossless: conversion.lossless,
|
|
90
|
+
losses: conversion.losses,
|
|
91
|
+
receipt: conversion.receipt,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
if (options.dryRun)
|
|
95
|
+
return report;
|
|
96
|
+
if (fs.existsSync(resolved) && !options.overwrite) {
|
|
97
|
+
throw new Error(`Refusing to overwrite existing shard ${resolved}; choose a new path or pass --force.`);
|
|
98
|
+
}
|
|
40
99
|
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
41
|
-
|
|
42
|
-
|
|
100
|
+
const temporary = `${resolved}.tmp-${process.pid}`;
|
|
101
|
+
try {
|
|
102
|
+
fs.writeFileSync(temporary, conversion.archive, { flag: "wx" });
|
|
103
|
+
if (options.overwrite) {
|
|
104
|
+
fs.renameSync(temporary, resolved);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
fs.linkSync(temporary, resolved);
|
|
108
|
+
fs.unlinkSync(temporary);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (fs.existsSync(temporary))
|
|
113
|
+
fs.unlinkSync(temporary);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
return report;
|
|
117
|
+
}
|
|
118
|
+
export function diagnoseAiwgFortemiShardMigration(cwd, inputPath) {
|
|
119
|
+
const resolved = path.resolve(cwd, inputPath);
|
|
120
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
121
|
+
throw new Error(`Legacy shard input does not exist or is not a regular file: ${resolved}`);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
supported: false,
|
|
125
|
+
inputPath: resolved,
|
|
126
|
+
mutationPlanned: false,
|
|
127
|
+
diagnostic: "Source-less core-v1 to full-v1 conversion is unsupported because omitted rich "
|
|
128
|
+
+ "components cannot be reconstructed without loss.",
|
|
129
|
+
action: "Regenerate from the source-backed AIWG index with "
|
|
130
|
+
+ "--schema-version 2.0.0 --profile full-v1; retain the legacy shard for rollback.",
|
|
131
|
+
};
|
|
43
132
|
}
|
|
44
133
|
//# sourceMappingURL=fortemi-shard-export.js.map
|
|
@@ -57,12 +57,13 @@ import { commandLogHandler } from './command-log.js';
|
|
|
57
57
|
import { skillUsageHandler } from './skill-usage.js';
|
|
58
58
|
import { modelsHandler } from './models.js';
|
|
59
59
|
import { versionsHandler } from './resource-versions.js';
|
|
60
|
+
import { jobHandler } from './job.js';
|
|
60
61
|
// Re-export individual handlers
|
|
61
62
|
export {
|
|
62
63
|
// Maintenance
|
|
63
64
|
helpHandler, versionHandler, doctorHandler, updateHandler, refreshHandler, regenerateHandler, workspaceContextHandler,
|
|
64
65
|
// Framework management
|
|
65
|
-
useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler,
|
|
66
|
+
useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler, jobHandler,
|
|
66
67
|
// Project
|
|
67
68
|
newBundleHandler, quickrefHandler, newProjectHandler, sessionHandler, sessionsHandler,
|
|
68
69
|
// Workspace
|
|
@@ -139,6 +140,7 @@ export const allHandlers = [
|
|
|
139
140
|
issueHandler,
|
|
140
141
|
issueAuditHandler,
|
|
141
142
|
runHandler,
|
|
143
|
+
jobHandler,
|
|
142
144
|
// Workspace management
|
|
143
145
|
...workspaceHandlers,
|
|
144
146
|
// Subcommand handlers (MCP, catalog, index, skills)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CodexJobExecutor } from '../../jobs/executor.js';
|
|
4
|
+
import { loadJobFlow, resolveWorkspaceFile } from '../../jobs/flow.js';
|
|
5
|
+
import { GiteaWorkItemClient } from '../../jobs/gitea.js';
|
|
6
|
+
import { renderExternalTrigger } from '../../jobs/render.js';
|
|
7
|
+
import { runExternalJob } from '../../jobs/runner.js';
|
|
8
|
+
import { AiwgError, EXIT_CODES, handlerResultFromError } from '../errors.js';
|
|
9
|
+
function option(args, name) {
|
|
10
|
+
const index = args.indexOf(name);
|
|
11
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
12
|
+
}
|
|
13
|
+
function usage(message) {
|
|
14
|
+
throw new AiwgError({ code: 'ERR_USAGE_JOB', message, exitCode: EXIT_CODES.USAGE, hint: 'Run `aiwg job help` for usage.' });
|
|
15
|
+
}
|
|
16
|
+
function help() {
|
|
17
|
+
console.log(`AIWG external-trigger jobs
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
aiwg job validate <flow>
|
|
21
|
+
aiwg job render-cron <flow> [--format cron|systemd|gitea-actions]
|
|
22
|
+
aiwg job run <flow> --once [--state-dir <absolute-path>] [--json]
|
|
23
|
+
|
|
24
|
+
The operating system or CI owns time. AIWG validates and executes one reviewed job.`);
|
|
25
|
+
}
|
|
26
|
+
async function validateFiles(flow) {
|
|
27
|
+
const workspace = await fs.realpath(flow.spec.executor.workspace);
|
|
28
|
+
if (workspace !== flow.spec.executor.workspace) {
|
|
29
|
+
throw new Error('executor.workspace must be canonical (no symlink or relative segments)');
|
|
30
|
+
}
|
|
31
|
+
const prompt = resolveWorkspaceFile(flow, flow.spec.executor.prompt);
|
|
32
|
+
const schema = resolveWorkspaceFile(flow, flow.spec.executor.resultSchema);
|
|
33
|
+
const [promptStat, schemaSource] = await Promise.all([fs.stat(prompt), fs.readFile(schema, 'utf8'), fs.access(flow.spec.executor.binary, constants.X_OK)]);
|
|
34
|
+
if (!promptStat.isFile())
|
|
35
|
+
throw new Error('executor.prompt must resolve to a regular file');
|
|
36
|
+
JSON.parse(schemaSource);
|
|
37
|
+
for (const root of flow.spec.security.approvedAttachmentRoots) {
|
|
38
|
+
const [canonical, stat] = await Promise.all([fs.realpath(root), fs.stat(root)]);
|
|
39
|
+
if (canonical !== root || !stat.isDirectory())
|
|
40
|
+
throw new Error('approvedAttachmentRoots must contain canonical directories');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function execute(ctx) {
|
|
44
|
+
try {
|
|
45
|
+
const [subcommand = 'help', flowArg] = ctx.args;
|
|
46
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
47
|
+
help();
|
|
48
|
+
return { exitCode: 0 };
|
|
49
|
+
}
|
|
50
|
+
if (!['validate', 'render-cron', 'run'].includes(subcommand))
|
|
51
|
+
usage(`Unknown job subcommand: ${subcommand}`);
|
|
52
|
+
if (!flowArg || flowArg.startsWith('-'))
|
|
53
|
+
usage(`job ${subcommand} requires a flow file`);
|
|
54
|
+
const loaded = await loadJobFlow(flowArg, ctx.cwd);
|
|
55
|
+
await validateFiles(loaded.flow);
|
|
56
|
+
if (subcommand === 'validate') {
|
|
57
|
+
console.log(JSON.stringify({ valid: true, apiVersion: loaded.flow.apiVersion, kind: loaded.flow.kind, name: loaded.flow.metadata.name }));
|
|
58
|
+
return { exitCode: 0 };
|
|
59
|
+
}
|
|
60
|
+
if (subcommand === 'render-cron') {
|
|
61
|
+
const format = (option(ctx.args.slice(2), '--format') ?? 'cron');
|
|
62
|
+
if (!['cron', 'systemd', 'gitea-actions'].includes(format))
|
|
63
|
+
usage(`Unsupported scheduler format: ${format}`);
|
|
64
|
+
console.log(renderExternalTrigger(loaded.flow, loaded.file, format));
|
|
65
|
+
return { exitCode: 0 };
|
|
66
|
+
}
|
|
67
|
+
if (!ctx.args.includes('--once'))
|
|
68
|
+
usage('job run requires --once; AIWG does not own a resident scheduler');
|
|
69
|
+
const stateRoot = option(ctx.args.slice(2), '--state-dir');
|
|
70
|
+
if (stateRoot && !stateRoot.startsWith('/'))
|
|
71
|
+
usage('--state-dir must be absolute');
|
|
72
|
+
if (stateRoot && path.resolve(stateRoot) === path.parse(path.resolve(stateRoot)).root)
|
|
73
|
+
usage('--state-dir must not be a filesystem root');
|
|
74
|
+
const client = await GiteaWorkItemClient.create(loaded.flow);
|
|
75
|
+
const result = await runExternalJob({
|
|
76
|
+
flow: loaded.flow,
|
|
77
|
+
client,
|
|
78
|
+
executor: new CodexJobExecutor(),
|
|
79
|
+
...(stateRoot ? { stateRoot } : {}),
|
|
80
|
+
signal: ctx.signal,
|
|
81
|
+
});
|
|
82
|
+
console.log(JSON.stringify(result, null, ctx.args.includes('--json') ? 2 : 0));
|
|
83
|
+
return { exitCode: result.status === 'failed-verification' ? EXIT_CODES.GENERAL : EXIT_CODES.OK };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return handlerResultFromError(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export const jobHandler = {
|
|
90
|
+
id: 'job',
|
|
91
|
+
name: 'External Job',
|
|
92
|
+
description: 'Validate, render, or run one externally triggered provider job',
|
|
93
|
+
category: 'orchestration',
|
|
94
|
+
aliases: ['job'],
|
|
95
|
+
execute,
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=job.js.map
|
|
@@ -258,7 +258,7 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
|
|
|
258
258
|
console.log(`Catalog: ${summary.catalogPath}`);
|
|
259
259
|
// Scheduler backend detection
|
|
260
260
|
const { execSync } = await import('child_process');
|
|
261
|
-
let schedulerBackend = '
|
|
261
|
+
let schedulerBackend = 'external trigger required (system cron/systemd/CI)';
|
|
262
262
|
let chronyInstalled = false;
|
|
263
263
|
try {
|
|
264
264
|
execSync('which chronyc 2>/dev/null || which chronyd 2>/dev/null', { stdio: 'pipe' });
|
|
@@ -273,7 +273,7 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
|
|
|
273
273
|
const isClaudeCodeContext = process.env.CLAUDE_CODE_VERSION !== undefined ||
|
|
274
274
|
process.env.ANTHROPIC_API_KEY !== undefined;
|
|
275
275
|
if (isClaudeCodeContext) {
|
|
276
|
-
schedulerBackend = 'native-cron (CronCreate)
|
|
276
|
+
schedulerBackend = 'native-cron (CronCreate); external trigger outside agent sessions';
|
|
277
277
|
}
|
|
278
278
|
console.log(`\nScheduler:`);
|
|
279
279
|
console.log(` Backend: ${schedulerBackend}`);
|
|
@@ -64,7 +64,7 @@ function parseServeArgs(args) {
|
|
|
64
64
|
// ============================================================
|
|
65
65
|
// WebSocket routing (#851)
|
|
66
66
|
//
|
|
67
|
-
// @hono/node-server
|
|
67
|
+
// @hono/node-server does not export createNodeWebSocket.
|
|
68
68
|
// We wire WebSocket routes directly via the Node.js HTTP server's
|
|
69
69
|
// 'upgrade' event and the `ws` npm package instead.
|
|
70
70
|
// ============================================================
|
|
@@ -550,7 +550,7 @@ export async function startServer(opts) {
|
|
|
550
550
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
551
551
|
const app = new Hono();
|
|
552
552
|
// WebSocket routes are handled via Node.js upgrade event below (see setupWebSockets).
|
|
553
|
-
// @hono/node-server
|
|
553
|
+
// @hono/node-server does not export createNodeWebSocket.
|
|
554
554
|
// Health check
|
|
555
555
|
app.get('/api/health', (c) => c.json({ status: 'ok', readOnly: opts.readOnly }));
|
|
556
556
|
// Connection status — server health, PTY sessions, sandboxes, subsystem status (#887)
|
|
@@ -28,6 +28,8 @@ Commands:
|
|
|
28
28
|
restore <session-id> Restore a reversible catalog tombstone
|
|
29
29
|
purge <session-id> Preview terminal AIWG-copy purge
|
|
30
30
|
audit --workspace <id> Read content-free mutation events
|
|
31
|
+
analytics <view> --workspace <id> Summary, tool-calls, escalations, or HITL
|
|
32
|
+
forensics <view> --workspace <id> Authorized timeline, indicators, or evidence
|
|
31
33
|
doctor Check catalog availability and integrity
|
|
32
34
|
|
|
33
35
|
Options:
|
|
@@ -219,6 +221,95 @@ async function executeCommand(ctx, args) {
|
|
|
219
221
|
nextCursor: result.nextCursor },
|
|
220
222
|
});
|
|
221
223
|
}
|
|
224
|
+
case 'analytics': {
|
|
225
|
+
const view = requiredPositional(args, 0, 'analytics view');
|
|
226
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
227
|
+
const query = analyticsQuery(args, workspaceId);
|
|
228
|
+
if (view === 'summary') {
|
|
229
|
+
return ok(command, {
|
|
230
|
+
view,
|
|
231
|
+
summary: repository.analyticsSummary(query),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const categories = analyticsCategories(view);
|
|
235
|
+
const items = repository.listAnalyticsFacts({ ...query, categories });
|
|
236
|
+
return ok(command, {
|
|
237
|
+
analyticsVersion: '1.0.0',
|
|
238
|
+
view,
|
|
239
|
+
items,
|
|
240
|
+
count: items.length,
|
|
241
|
+
groupBy: analyticsGrouping(items, args.values.get('--group-by')),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
case 'forensics': {
|
|
245
|
+
if (!args.flags.has('--authorize-forensics')) {
|
|
246
|
+
throw new CliError('OPERATION_NOT_AUTHORIZED', 'forensic extraction requires --authorize-forensics for this invocation', EXIT.usage);
|
|
247
|
+
}
|
|
248
|
+
const view = requiredPositional(args, 0, 'forensics view');
|
|
249
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
250
|
+
const query = analyticsQuery(args, workspaceId);
|
|
251
|
+
if (view === 'indicators') {
|
|
252
|
+
const items = repository.listAnalyticsFacts({
|
|
253
|
+
...query,
|
|
254
|
+
categories: ['indicator'],
|
|
255
|
+
});
|
|
256
|
+
return ok(command, forensicOutput(view, items, args));
|
|
257
|
+
}
|
|
258
|
+
if (view === 'timeline') {
|
|
259
|
+
const target = requiredPositional(args, 1, 'session-id or query');
|
|
260
|
+
const direct = repository.getSession(target, workspaceId);
|
|
261
|
+
const sessionIds = direct
|
|
262
|
+
? [target]
|
|
263
|
+
: [...new Set(repository.search({
|
|
264
|
+
query: target,
|
|
265
|
+
workspaceId,
|
|
266
|
+
limit: boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit'),
|
|
267
|
+
}).items.map((item) => item.sessionId))];
|
|
268
|
+
const items = sessionIds.flatMap((sessionId) => repository.listAnalyticsFacts({ ...query, sessionId }));
|
|
269
|
+
return ok(command, forensicOutput(view, items, args));
|
|
270
|
+
}
|
|
271
|
+
if (view === 'evidence') {
|
|
272
|
+
const id = requiredPositional(args, 1, 'event-id, fact-id, or candidate-id');
|
|
273
|
+
const evidence = repository.getAnalyticsEvidence(id, workspaceId);
|
|
274
|
+
if (evidence.fact && evidence.event) {
|
|
275
|
+
return ok(command, {
|
|
276
|
+
analyticsVersion: '1.0.0',
|
|
277
|
+
view,
|
|
278
|
+
fact: evidence.fact,
|
|
279
|
+
event: {
|
|
280
|
+
eventId: evidence.event.eventId,
|
|
281
|
+
sessionId: evidence.event.sessionId,
|
|
282
|
+
sourceId: evidence.event.sourceId,
|
|
283
|
+
importRunId: evidence.event.importRunId,
|
|
284
|
+
sequence: evidence.event.sequence,
|
|
285
|
+
kind: evidence.event.kind,
|
|
286
|
+
occurredAt: evidence.event.occurredAt,
|
|
287
|
+
sensitivity: evidence.event.sensitivity,
|
|
288
|
+
rawReference: evidence.event.rawReference,
|
|
289
|
+
digest: evidence.event.digest,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const candidate = repository.getCandidate(id, undefined, workspaceId);
|
|
294
|
+
if (!candidate) {
|
|
295
|
+
throw new CliError('EVIDENCE_NOT_FOUND', `authorized analytics evidence not found: ${id}`, EXIT.unavailable);
|
|
296
|
+
}
|
|
297
|
+
return ok(command, {
|
|
298
|
+
analyticsVersion: '1.0.0',
|
|
299
|
+
view,
|
|
300
|
+
candidate: {
|
|
301
|
+
candidateId: candidate.candidateId,
|
|
302
|
+
version: candidate.version,
|
|
303
|
+
type: candidate.type,
|
|
304
|
+
evidence: candidate.evidence.map(({ quote: _quote, ...citation }) => citation),
|
|
305
|
+
sensitivity: candidate.sensitivity,
|
|
306
|
+
security: candidate.security,
|
|
307
|
+
reviewState: candidate.reviewState,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
throw new CliError('INVALID_ARGUMENT', `unknown forensics view: ${view}`, EXIT.usage);
|
|
312
|
+
}
|
|
222
313
|
case 'extract': {
|
|
223
314
|
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
224
315
|
const sessionId = args.positionals[0];
|
|
@@ -997,6 +1088,7 @@ function parseArgs(argv) {
|
|
|
997
1088
|
'--manifest', '--provider-home', '--codex-root', '--lock-wait-ms', '--min-coverage', '--gap',
|
|
998
1089
|
'--inactivity-threshold',
|
|
999
1090
|
'--control-events',
|
|
1091
|
+
'--session', '--status', '--actor', '--group-by',
|
|
1000
1092
|
]);
|
|
1001
1093
|
let command;
|
|
1002
1094
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -1184,6 +1276,96 @@ function dependentAction(value) {
|
|
|
1184
1276
|
}
|
|
1185
1277
|
return value;
|
|
1186
1278
|
}
|
|
1279
|
+
function analyticsQuery(args, workspaceId) {
|
|
1280
|
+
const providerInput = args.values.get('--provider');
|
|
1281
|
+
const statusInput = args.values.get('--status');
|
|
1282
|
+
return {
|
|
1283
|
+
workspaceId,
|
|
1284
|
+
provider: providerInput ? assertSessionProviderId(providerInput) : undefined,
|
|
1285
|
+
sessionId: args.values.get('--session'),
|
|
1286
|
+
dateFrom: args.values.get('--date-from'),
|
|
1287
|
+
dateTo: args.values.get('--date-to'),
|
|
1288
|
+
tool: args.values.get('--tool'),
|
|
1289
|
+
status: statusInput ? analyticsStatus(statusInput) : undefined,
|
|
1290
|
+
actor: args.values.get('--actor') ?? args.values.get('--participant'),
|
|
1291
|
+
tag: args.values.get('--tag'),
|
|
1292
|
+
sensitivity: args.values.get('--sensitivity'),
|
|
1293
|
+
extractionState: args.values.get('--extraction-state'),
|
|
1294
|
+
limit: boundedInteger(args.values.get('--limit'), 500, 1, 5_000, '--limit'),
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function analyticsStatus(value) {
|
|
1298
|
+
const allowed = new Set([
|
|
1299
|
+
'requested', 'running', 'succeeded', 'failed', 'granted', 'denied',
|
|
1300
|
+
'timed-out', 'unsupported', 'provider-unknown', 'observed',
|
|
1301
|
+
]);
|
|
1302
|
+
if (!allowed.has(value)) {
|
|
1303
|
+
throw new CliError('INVALID_ARGUMENT', `invalid analytics status: ${value}`, EXIT.usage);
|
|
1304
|
+
}
|
|
1305
|
+
return value;
|
|
1306
|
+
}
|
|
1307
|
+
function analyticsCategories(view) {
|
|
1308
|
+
if (view === 'tool-calls')
|
|
1309
|
+
return ['tool-call', 'tool-result'];
|
|
1310
|
+
if (view === 'escalations')
|
|
1311
|
+
return ['escalation'];
|
|
1312
|
+
if (view === 'hitl')
|
|
1313
|
+
return ['hitl'];
|
|
1314
|
+
throw new CliError('INVALID_ARGUMENT', `unknown analytics view: ${view}`, EXIT.usage);
|
|
1315
|
+
}
|
|
1316
|
+
function analyticsGrouping(items, groupBy) {
|
|
1317
|
+
if (!groupBy)
|
|
1318
|
+
return null;
|
|
1319
|
+
if (!['tool', 'session', 'provider'].includes(groupBy)) {
|
|
1320
|
+
throw new CliError('INVALID_ARGUMENT', `invalid --group-by value: ${groupBy}`, EXIT.usage);
|
|
1321
|
+
}
|
|
1322
|
+
const counts = {};
|
|
1323
|
+
for (const item of items) {
|
|
1324
|
+
const value = groupBy === 'tool'
|
|
1325
|
+
? item.toolName
|
|
1326
|
+
: groupBy === 'session'
|
|
1327
|
+
? item.sessionId
|
|
1328
|
+
: item.provider;
|
|
1329
|
+
const key = typeof value === 'string' && value ? value : '<unknown>';
|
|
1330
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1331
|
+
}
|
|
1332
|
+
return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
|
|
1333
|
+
}
|
|
1334
|
+
function forensicOutput(view, items, args) {
|
|
1335
|
+
const output = {
|
|
1336
|
+
analyticsVersion: '1.0.0',
|
|
1337
|
+
view,
|
|
1338
|
+
items,
|
|
1339
|
+
count: items.length,
|
|
1340
|
+
authorization: {
|
|
1341
|
+
explicit: true,
|
|
1342
|
+
providerLogsModified: false,
|
|
1343
|
+
historicalContentExecuted: false,
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
if (args.flags.has('--markdown')) {
|
|
1347
|
+
output.markdown = [
|
|
1348
|
+
`# Session Forensics ${view}`,
|
|
1349
|
+
'',
|
|
1350
|
+
`Facts: ${items.length}`,
|
|
1351
|
+
'',
|
|
1352
|
+
'| Time | Provider | Session | Category | Status | Evidence |',
|
|
1353
|
+
'|---|---|---|---|---|---|',
|
|
1354
|
+
...items.map((item) => {
|
|
1355
|
+
const citation = item.sourceCitation;
|
|
1356
|
+
return [
|
|
1357
|
+
item.occurredAt ?? '<unknown>',
|
|
1358
|
+
item.provider ?? '<unknown>',
|
|
1359
|
+
item.sessionId ?? '<unknown>',
|
|
1360
|
+
item.category ?? '<unknown>',
|
|
1361
|
+
item.status ?? '<unknown>',
|
|
1362
|
+
citation?.eventId ?? item.eventId ?? '<unknown>',
|
|
1363
|
+
].map((value) => String(value).replaceAll('|', '\\|')).join(' | ');
|
|
1364
|
+
}).map((row) => `| ${row} |`),
|
|
1365
|
+
].join('\n');
|
|
1366
|
+
}
|
|
1367
|
+
return output;
|
|
1368
|
+
}
|
|
1187
1369
|
function isDryRun(ctx, args) {
|
|
1188
1370
|
return Boolean(ctx.dryRun || args.flags.has('--dry-run'));
|
|
1189
1371
|
}
|
|
@@ -1202,6 +1384,12 @@ function emit(value) {
|
|
|
1202
1384
|
function printHuman(value) {
|
|
1203
1385
|
if (value.status === 'preview')
|
|
1204
1386
|
console.log('Preview (no changes applied)');
|
|
1387
|
+
if (value.command === 'sessions.forensics' && value.data
|
|
1388
|
+
&& typeof value.data === 'object' && 'markdown' in value.data
|
|
1389
|
+
&& typeof value.data.markdown === 'string') {
|
|
1390
|
+
console.log(value.data.markdown);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1205
1393
|
if (value.command === 'sessions.timeline' && value.data
|
|
1206
1394
|
&& typeof value.data === 'object' && 'items' in value.data) {
|
|
1207
1395
|
const data = value.data;
|