hadara 0.1.0-rc.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 +21 -0
- package/README.md +109 -0
- package/dist/agent/evidence.js +50 -0
- package/dist/agent/loop.js +124 -0
- package/dist/cli/args.js +70 -0
- package/dist/cli/dashboard.js +185 -0
- package/dist/cli/debt.js +41 -0
- package/dist/cli/doctor.js +68 -0
- package/dist/cli/errors.js +58 -0
- package/dist/cli/evidence-json.js +75 -0
- package/dist/cli/evidence.js +80 -0
- package/dist/cli/handoff.js +16 -0
- package/dist/cli/harness.js +57 -0
- package/dist/cli/hermes-json.js +31 -0
- package/dist/cli/hermes.js +28 -0
- package/dist/cli/init.js +142 -0
- package/dist/cli/install.js +34 -0
- package/dist/cli/main.js +216 -0
- package/dist/cli/mcp.js +15 -0
- package/dist/cli/package-smoke.js +37 -0
- package/dist/cli/policy-json.js +22 -0
- package/dist/cli/policy.js +43 -0
- package/dist/cli/release-artifact.js +47 -0
- package/dist/cli/release-dry-run.js +24 -0
- package/dist/cli/release-gate.js +28 -0
- package/dist/cli/release-publish.js +41 -0
- package/dist/cli/run-scaffold.js +68 -0
- package/dist/cli/run-state.js +41 -0
- package/dist/cli/run.js +191 -0
- package/dist/cli/smoke.js +58 -0
- package/dist/cli/status-json.js +6 -0
- package/dist/cli/status.js +26 -0
- package/dist/cli/task-json.js +8 -0
- package/dist/cli/task.js +64 -0
- package/dist/cli/tools.js +25 -0
- package/dist/cli/tui.js +72 -0
- package/dist/cli/write-preflight.js +27 -0
- package/dist/core/audit.js +41 -0
- package/dist/core/events.js +63 -0
- package/dist/core/fs.js +44 -0
- package/dist/core/paths.js +59 -0
- package/dist/core/redaction.js +178 -0
- package/dist/core/schema.js +253 -0
- package/dist/core/workspace.js +47 -0
- package/dist/evidence/evidence.js +170 -0
- package/dist/evidence/private-manifest.js +101 -0
- package/dist/handoff/handoff.js +49 -0
- package/dist/harness/replay.js +200 -0
- package/dist/harness/validate.js +465 -0
- package/dist/hermes/context-export.js +104 -0
- package/dist/index.js +29 -0
- package/dist/mcp/server.js +104 -0
- package/dist/mcp/tool-dispatch.js +159 -0
- package/dist/mcp/tool-registry.js +150 -0
- package/dist/mcp/tool-schemas.js +18 -0
- package/dist/policy/command-risk.js +39 -0
- package/dist/policy/permission-matrix.js +42 -0
- package/dist/policy/policy.js +20 -0
- package/dist/policy/preflight.js +47 -0
- package/dist/policy/presets.js +24 -0
- package/dist/policy/tokenizer.js +53 -0
- package/dist/providers/fallback-executor.js +46 -0
- package/dist/providers/mock-provider.js +49 -0
- package/dist/providers/provider-contract.js +2 -0
- package/dist/providers/provider-preparation.js +220 -0
- package/dist/providers/scripted-provider.js +69 -0
- package/dist/schemas/active-run-projection.schema.json +73 -0
- package/dist/schemas/active-run-resume.schema.json +68 -0
- package/dist/schemas/clean-checkout-smoke.schema.json +126 -0
- package/dist/schemas/context-export.schema.json +35 -0
- package/dist/schemas/event.schema.json +17 -0
- package/dist/schemas/evidence-list.schema.json +49 -0
- package/dist/schemas/feature-smoke.schema.json +67 -0
- package/dist/schemas/install-plan.schema.json +93 -0
- package/dist/schemas/package-smoke.schema.json +130 -0
- package/dist/schemas/private-evidence.schema.json +48 -0
- package/dist/schemas/provider-call.schema.json +42 -0
- package/dist/schemas/provider-config.schema.json +43 -0
- package/dist/schemas/release-artifact-manifest.schema.json +55 -0
- package/dist/schemas/release-artifact.schema.json +140 -0
- package/dist/schemas/release-dry-run.schema.json +141 -0
- package/dist/schemas/release-gate.schema.json +42 -0
- package/dist/schemas/release-publish.schema.json +114 -0
- package/dist/schemas/schema-index.json +145 -0
- package/dist/schemas/smoke-evidence-summary.schema.json +88 -0
- package/dist/schemas/tools-list.schema.json +78 -0
- package/dist/schemas/write-preflight.schema.json +47 -0
- package/dist/services/active-run-state.js +215 -0
- package/dist/services/capability-registry.js +540 -0
- package/dist/services/clean-checkout-smoke.js +393 -0
- package/dist/services/evidence-list.js +136 -0
- package/dist/services/feature-smoke.js +155 -0
- package/dist/services/harness-service.js +7 -0
- package/dist/services/install-plan.js +233 -0
- package/dist/services/operational-debt.js +767 -0
- package/dist/services/operations-status-service.js +195 -0
- package/dist/services/package-smoke.js +676 -0
- package/dist/services/policy-service.js +25 -0
- package/dist/services/project-read-model.js +101 -0
- package/dist/services/release-artifact-evidence.js +77 -0
- package/dist/services/release-artifact.js +351 -0
- package/dist/services/release-dry-run.js +253 -0
- package/dist/services/release-evidence.js +138 -0
- package/dist/services/release-publish.js +163 -0
- package/dist/services/smoke-evidence.js +104 -0
- package/dist/services/task-read-model.js +125 -0
- package/dist/services/tools-list.js +26 -0
- package/dist/services/write-preflight.js +240 -0
- package/dist/task/task-capsule.js +121 -0
- package/dist/tools/fake-shell.js +56 -0
- package/dist/tui/cache.js +341 -0
- package/dist/tui/constants.js +44 -0
- package/dist/tui/layout.js +140 -0
- package/dist/tui/markdown.js +238 -0
- package/dist/tui/read-model-worker.js +24 -0
- package/dist/tui/read-model.js +502 -0
- package/dist/tui/snapshot.js +434 -0
- package/dist/tui/state.js +229 -0
- package/dist/tui/terminal.js +475 -0
- package/dist/tui/theme.js +86 -0
- package/package.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ictseoyoungmin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# HADARA
|
|
2
|
+
|
|
3
|
+
**HADARA** is a portable agentic development workbench.
|
|
4
|
+
|
|
5
|
+
> Unbroken Context, Verified Development.
|
|
6
|
+
|
|
7
|
+
HADARA is named from **Harness + Dara**. A harness safely binds and controls complex systems; Dara carries layered associations of holding, wisdom, durability, and continuity. HADARA binds non-deterministic LLM agent work into a production-oriented workflow through Task Capsules, Session Continuity, Policy Layers, Evidence Logs, and Handoff Protocols.
|
|
8
|
+
|
|
9
|
+
This repository is a **bootstrap skeleton** for developing HADARA using the HADARA protocol itself.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm run dev -- doctor
|
|
16
|
+
npm run dev -- task create "implement ProviderClient contract"
|
|
17
|
+
npm run dev -- task list
|
|
18
|
+
npm test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Linux/macOS portable launcher:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
chmod +x ./start.sh
|
|
25
|
+
./start.sh doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Windows launcher:
|
|
29
|
+
|
|
30
|
+
```bat
|
|
31
|
+
START.bat doctor
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Development Principle
|
|
35
|
+
|
|
36
|
+
HADARA development must dogfood the HADARA workflow:
|
|
37
|
+
|
|
38
|
+
1. Read `docs/PROJECT_STATE.md`
|
|
39
|
+
2. Read `docs/AGENT_HANDOFF.md`
|
|
40
|
+
3. Read `docs/TASK_BOARD.md`
|
|
41
|
+
4. Work inside a Task Capsule
|
|
42
|
+
5. Attach evidence before marking work complete
|
|
43
|
+
6. Update handoff before stopping
|
|
44
|
+
|
|
45
|
+
## Store Separation
|
|
46
|
+
|
|
47
|
+
HADARA separates **portable runtime state** from **project-owned development state**.
|
|
48
|
+
|
|
49
|
+
### Portable / USB Store
|
|
50
|
+
|
|
51
|
+
Located under the HADARA installation root:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
data/
|
|
55
|
+
config/
|
|
56
|
+
secrets/
|
|
57
|
+
sessions/
|
|
58
|
+
logs/
|
|
59
|
+
audit/
|
|
60
|
+
cache/
|
|
61
|
+
exports/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is **not committed**. It is local, portable, and may live on a USB drive.
|
|
65
|
+
|
|
66
|
+
### Project Repo Store
|
|
67
|
+
|
|
68
|
+
Located inside each project repository:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
docs/
|
|
72
|
+
tasks/
|
|
73
|
+
.hadara/
|
|
74
|
+
AGENTS.md
|
|
75
|
+
.hermes.md
|
|
76
|
+
HERMES.md
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This is committed when it represents reproducible project state, conventions, or agent context.
|
|
80
|
+
|
|
81
|
+
## Initial CLI Commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
hadara init
|
|
85
|
+
hadara doctor
|
|
86
|
+
hadara task create "..."
|
|
87
|
+
hadara task list
|
|
88
|
+
hadara task show T-0001
|
|
89
|
+
hadara evidence collect --task T-0001
|
|
90
|
+
hadara handoff update --task T-0001
|
|
91
|
+
hadara hermes detect
|
|
92
|
+
hadara hermes export-context
|
|
93
|
+
hadara mcp serve
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Current CLI is a seed implementation. Full agent execution, provider integration, dashboard, and MCP server runtime are intentionally stubbed for later tasks.
|
|
97
|
+
|
|
98
|
+
## Test Suites
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run test:unit
|
|
102
|
+
npm run test:contract
|
|
103
|
+
npm run test:harness
|
|
104
|
+
npm run check
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
TBD. Recommended candidates: Apache-2.0 or MIT.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.attachAgentLoopEvidence = attachAgentLoopEvidence;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const evidence_1 = require("../evidence/evidence");
|
|
9
|
+
function attachAgentLoopEvidence(projectRoot, result) {
|
|
10
|
+
if (!result.taskId)
|
|
11
|
+
return [];
|
|
12
|
+
const fakeShellSteps = result.steps.filter(isFakeShellStep);
|
|
13
|
+
if (fakeShellSteps.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const summary = `Agent loop captured ${fakeShellSteps.length} fake-shell observation${fakeShellSteps.length === 1 ? '' : 's'}.`;
|
|
16
|
+
const appended = (0, evidence_1.appendEvidenceTextArtifact)(projectRoot, {
|
|
17
|
+
taskId: result.taskId,
|
|
18
|
+
kind: 'command-log',
|
|
19
|
+
summary,
|
|
20
|
+
result: result.ok ? 'passed' : 'failed',
|
|
21
|
+
visibility: 'public'
|
|
22
|
+
}, {
|
|
23
|
+
fileName: 'agent-loop-fake-shell-observations.jsonl',
|
|
24
|
+
content: `${fakeShellSteps.map((step) => JSON.stringify(toObservationRecord(step))).join('\n')}\n`
|
|
25
|
+
});
|
|
26
|
+
return [toAttachment(projectRoot, appended, summary, result.ok ? 'passed' : 'failed')];
|
|
27
|
+
}
|
|
28
|
+
function isFakeShellStep(step) {
|
|
29
|
+
return step.type === 'tool' && step.tool === 'fake_shell';
|
|
30
|
+
}
|
|
31
|
+
function toObservationRecord(step) {
|
|
32
|
+
return {
|
|
33
|
+
step: step.step,
|
|
34
|
+
tool: step.tool,
|
|
35
|
+
ok: step.ok,
|
|
36
|
+
observation: step.observation
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function toAttachment(projectRoot, appended, summary, result) {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'command-log',
|
|
42
|
+
summary,
|
|
43
|
+
result,
|
|
44
|
+
evidencePath: appended.evidence.evidencePath ?? '',
|
|
45
|
+
markdownPath: toPortablePath(node_path_1.default.relative(projectRoot, appended.markdownPath))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function toPortablePath(value) {
|
|
49
|
+
return value.split(node_path_1.default.sep).join('/');
|
|
50
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAgentLoop = runAgentLoop;
|
|
4
|
+
const policy_1 = require("../policy/policy");
|
|
5
|
+
const fake_shell_1 = require("../tools/fake-shell");
|
|
6
|
+
async function runAgentLoop(input) {
|
|
7
|
+
const mode = (0, policy_1.parsePermissionMode)(input.mode ?? 'assisted');
|
|
8
|
+
const maxSteps = input.maxSteps ?? 6;
|
|
9
|
+
const fixtures = input.fakeShellFixtures ?? {};
|
|
10
|
+
const steps = [];
|
|
11
|
+
const issues = [];
|
|
12
|
+
const messages = [
|
|
13
|
+
{
|
|
14
|
+
role: 'system',
|
|
15
|
+
content: 'You are running inside HADARA deterministic harness mode. Request fake shell tools with JSON only.'
|
|
16
|
+
},
|
|
17
|
+
{ role: 'user', content: input.request }
|
|
18
|
+
];
|
|
19
|
+
for (let step = 1; step <= maxSteps; step += 1) {
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await input.provider.chat({ messages });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
issues.push({
|
|
26
|
+
severity: 'error',
|
|
27
|
+
code: 'AGENT_PROVIDER_FAILED',
|
|
28
|
+
message: normalizeProviderFailure(input.provider, error).message,
|
|
29
|
+
step
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion: 'hadara.agent.loop.v1',
|
|
33
|
+
command: 'agent.loop',
|
|
34
|
+
ok: false,
|
|
35
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
36
|
+
mode,
|
|
37
|
+
request: input.request,
|
|
38
|
+
steps,
|
|
39
|
+
issues
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
steps.push({ step, type: 'assistant', ok: response.finishReason !== 'error', response });
|
|
43
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
44
|
+
const toolRequest = parseFakeShellToolRequest(response.content);
|
|
45
|
+
if (!toolRequest) {
|
|
46
|
+
const toolFailed = steps.some((item) => item.type === 'tool' && !item.ok);
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: 'hadara.agent.loop.v1',
|
|
49
|
+
command: 'agent.loop',
|
|
50
|
+
ok: !toolFailed && issues.every((issue) => issue.severity !== 'error') && response.finishReason !== 'error',
|
|
51
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
52
|
+
mode,
|
|
53
|
+
request: input.request,
|
|
54
|
+
finalResponse: response.content,
|
|
55
|
+
steps,
|
|
56
|
+
issues
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const observation = (0, fake_shell_1.runFakeShellCommand)({
|
|
60
|
+
command: toolRequest.command,
|
|
61
|
+
mode,
|
|
62
|
+
fixtures
|
|
63
|
+
});
|
|
64
|
+
steps.push({
|
|
65
|
+
step,
|
|
66
|
+
type: 'tool',
|
|
67
|
+
ok: observation.ok,
|
|
68
|
+
tool: 'fake_shell',
|
|
69
|
+
observation
|
|
70
|
+
});
|
|
71
|
+
messages.push({ role: 'tool', content: JSON.stringify(observation) });
|
|
72
|
+
if (!observation.ok) {
|
|
73
|
+
issues.push({
|
|
74
|
+
severity: 'error',
|
|
75
|
+
code: `FAKE_SHELL_${observation.result.status.toUpperCase()}`,
|
|
76
|
+
message: observation.result.reason ?? observation.result.stderr,
|
|
77
|
+
step
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
issues.push({
|
|
82
|
+
severity: 'error',
|
|
83
|
+
code: 'AGENT_LOOP_MAX_STEPS_EXCEEDED',
|
|
84
|
+
message: `Agent loop exceeded maxSteps (${maxSteps}) without a final response.`
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: 'hadara.agent.loop.v1',
|
|
88
|
+
command: 'agent.loop',
|
|
89
|
+
ok: false,
|
|
90
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
91
|
+
mode,
|
|
92
|
+
request: input.request,
|
|
93
|
+
steps,
|
|
94
|
+
issues
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parseFakeShellToolRequest(content) {
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(content);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (!parsed || typeof parsed !== 'object')
|
|
106
|
+
return null;
|
|
107
|
+
const record = parsed;
|
|
108
|
+
if (record.type === 'tool_request' && record.tool === 'fake_shell' && typeof record.command === 'string') {
|
|
109
|
+
return {
|
|
110
|
+
type: 'tool_request',
|
|
111
|
+
tool: 'fake_shell',
|
|
112
|
+
command: record.command
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function normalizeProviderFailure(provider, error) {
|
|
118
|
+
if (isProviderError(error))
|
|
119
|
+
return error;
|
|
120
|
+
return provider.normalizeError(error);
|
|
121
|
+
}
|
|
122
|
+
function isProviderError(error) {
|
|
123
|
+
return Boolean(error && typeof error === 'object' && 'provider' in error && 'code' in error && 'retriable' in error);
|
|
124
|
+
}
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CliArgsError = void 0;
|
|
4
|
+
exports.getStringOption = getStringOption;
|
|
5
|
+
exports.getRequiredStringOption = getRequiredStringOption;
|
|
6
|
+
exports.getIntegerOption = getIntegerOption;
|
|
7
|
+
exports.getFlag = getFlag;
|
|
8
|
+
exports.rejectMissingValue = rejectMissingValue;
|
|
9
|
+
exports.rejectValueThatLooksLikeFlag = rejectValueThatLooksLikeFlag;
|
|
10
|
+
class CliArgsError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = 'CliArgsError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.CliArgsError = CliArgsError;
|
|
19
|
+
function getStringOption(args, name, fallback) {
|
|
20
|
+
const index = args.indexOf(name);
|
|
21
|
+
if (index < 0)
|
|
22
|
+
return fallback;
|
|
23
|
+
const value = args[index + 1];
|
|
24
|
+
rejectMissingValue(name, value);
|
|
25
|
+
rejectValueThatLooksLikeFlag(name, value);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function getRequiredStringOption(args, name) {
|
|
29
|
+
const value = getStringOption(args, name);
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
throw new CliArgsError('CLI_OPTION_REQUIRED', `${name} is required`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function getIntegerOption(args, name, bounds = {}) {
|
|
36
|
+
const fallback = bounds.fallback === undefined ? undefined : String(bounds.fallback);
|
|
37
|
+
const value = getStringOption(args, name, fallback);
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (!/^\d+$/.test(value)) {
|
|
41
|
+
throw new CliArgsError('CLI_OPTION_INTEGER_INVALID', integerMessage(name, bounds));
|
|
42
|
+
}
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
if (!Number.isSafeInteger(parsed) || (bounds.min !== undefined && parsed < bounds.min) || (bounds.max !== undefined && parsed > bounds.max)) {
|
|
45
|
+
throw new CliArgsError('CLI_OPTION_INTEGER_INVALID', integerMessage(name, bounds));
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
function getFlag(args, name) {
|
|
50
|
+
return args.includes(name);
|
|
51
|
+
}
|
|
52
|
+
function rejectMissingValue(name, value) {
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
throw new CliArgsError('CLI_OPTION_MISSING_VALUE', `${name} requires a value`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function rejectValueThatLooksLikeFlag(name, value) {
|
|
58
|
+
if (value.startsWith('--')) {
|
|
59
|
+
throw new CliArgsError('CLI_OPTION_VALUE_LOOKS_LIKE_FLAG', `${name} value must not look like a flag`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function integerMessage(name, bounds) {
|
|
63
|
+
if (bounds.min !== undefined && bounds.max !== undefined)
|
|
64
|
+
return `${name} must be an integer from ${bounds.min} to ${bounds.max}`;
|
|
65
|
+
if (bounds.min !== undefined)
|
|
66
|
+
return `${name} must be an integer greater than or equal to ${bounds.min}`;
|
|
67
|
+
if (bounds.max !== undefined)
|
|
68
|
+
return `${name} must be an integer less than or equal to ${bounds.max}`;
|
|
69
|
+
return `${name} must be an integer`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleDashboardCommand = handleDashboardCommand;
|
|
7
|
+
exports.serveDashboard = serveDashboard;
|
|
8
|
+
exports.createDashboardStaticResponse = createDashboardStaticResponse;
|
|
9
|
+
exports.createDashboardServerResponse = createDashboardServerResponse;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const active_run_state_1 = require("../services/active-run-state");
|
|
14
|
+
const evidence_list_1 = require("../services/evidence-list");
|
|
15
|
+
const operational_debt_1 = require("../services/operational-debt");
|
|
16
|
+
const operations_status_service_1 = require("../services/operations-status-service");
|
|
17
|
+
const task_read_model_1 = require("../services/task-read-model");
|
|
18
|
+
const args_1 = require("./args");
|
|
19
|
+
const DASHBOARD_HTML = node_path_1.default.join('docs', 'design', 'dashboard', 'index.html');
|
|
20
|
+
const DASHBOARD_FIXTURE = node_path_1.default.join('docs', 'design', 'fixtures', 'hadara.ops.status.sample.json');
|
|
21
|
+
function handleDashboardCommand(input) {
|
|
22
|
+
if (input.args[0] !== 'dashboard' || input.args[1] !== 'serve')
|
|
23
|
+
return false;
|
|
24
|
+
const host = (0, args_1.getStringOption)(input.args, '--host', '127.0.0.1');
|
|
25
|
+
const port = (0, args_1.getIntegerOption)(input.args, '--port', { fallback: 4173, min: 1, max: 65535 });
|
|
26
|
+
serveDashboard(input.projectRoot, { host, port });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
function serveDashboard(projectRoot, options = {}) {
|
|
30
|
+
const host = options.host ?? '127.0.0.1';
|
|
31
|
+
const port = options.port ?? 4173;
|
|
32
|
+
const server = node_http_1.default.createServer((request, response) => {
|
|
33
|
+
let staticResponse;
|
|
34
|
+
try {
|
|
35
|
+
staticResponse = createDashboardServerResponse(projectRoot, request.url ?? '/', request.method ?? 'GET');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
staticResponse = internalError();
|
|
39
|
+
}
|
|
40
|
+
response.writeHead(staticResponse.statusCode, staticResponse.headers);
|
|
41
|
+
response.end(staticResponse.body);
|
|
42
|
+
});
|
|
43
|
+
server.listen(port, host, () => {
|
|
44
|
+
const address = server.address();
|
|
45
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
46
|
+
// console.log(`[HADARA] Dashboard serving sample fixture at http://${host}:${actualPort}/dashboard/`);
|
|
47
|
+
console.log(`[HADARA] Dashboard serving at http://${host}:${actualPort}/dashboard/ with read-only APIs under /api/.`);
|
|
48
|
+
});
|
|
49
|
+
return server;
|
|
50
|
+
}
|
|
51
|
+
function createDashboardStaticResponse(projectRoot, requestUrl, method = 'GET') {
|
|
52
|
+
const normalizedMethod = method.toUpperCase();
|
|
53
|
+
if (normalizedMethod !== 'GET' && normalizedMethod !== 'HEAD')
|
|
54
|
+
return methodNotAllowed();
|
|
55
|
+
const pathname = safePathname(requestUrl);
|
|
56
|
+
if (!pathname)
|
|
57
|
+
return notFound();
|
|
58
|
+
const headOnly = normalizedMethod === 'HEAD';
|
|
59
|
+
if (pathname === '/' || pathname === '/dashboard' || pathname === '/dashboard/' || pathname === '/dashboard/index.html') {
|
|
60
|
+
return fileResponse(projectRoot, DASHBOARD_HTML, 'text/html; charset=utf-8', headOnly);
|
|
61
|
+
}
|
|
62
|
+
if (pathname === '/fixtures/hadara.ops.status.sample.json' || pathname === '/dashboard/fixtures/hadara.ops.status.sample.json') {
|
|
63
|
+
return fileResponse(projectRoot, DASHBOARD_FIXTURE, 'application/json; charset=utf-8', headOnly);
|
|
64
|
+
}
|
|
65
|
+
return notFound();
|
|
66
|
+
}
|
|
67
|
+
function createDashboardServerResponse(projectRoot, requestUrl, method = 'GET') {
|
|
68
|
+
try {
|
|
69
|
+
const apiResponse = createDashboardApiResponse(projectRoot, requestUrl, method);
|
|
70
|
+
if (apiResponse)
|
|
71
|
+
return apiResponse;
|
|
72
|
+
return createDashboardStaticResponse(projectRoot, requestUrl, method);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return internalError();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function createDashboardApiResponse(projectRoot, requestUrl, method) {
|
|
79
|
+
const normalizedMethod = method.toUpperCase();
|
|
80
|
+
if (normalizedMethod !== 'GET' && normalizedMethod !== 'HEAD')
|
|
81
|
+
return null;
|
|
82
|
+
const url = safeUrl(requestUrl);
|
|
83
|
+
if (!url || !url.pathname.startsWith('/api/'))
|
|
84
|
+
return null;
|
|
85
|
+
const headOnly = normalizedMethod === 'HEAD';
|
|
86
|
+
if (url.pathname === '/api/status')
|
|
87
|
+
return jsonResponse((0, operations_status_service_1.createOpsStatusReport)(projectRoot), headOnly);
|
|
88
|
+
if (url.pathname === '/api/tasks')
|
|
89
|
+
return jsonResponse((0, task_read_model_1.createTaskListReport)(projectRoot), headOnly);
|
|
90
|
+
if (url.pathname === '/api/active-run')
|
|
91
|
+
return jsonResponse((0, active_run_state_1.safeCreateActiveRunProjection)(projectRoot), headOnly);
|
|
92
|
+
if (url.pathname === '/api/debt')
|
|
93
|
+
return jsonResponse((0, operational_debt_1.createOperationalDebtReport)(projectRoot), headOnly);
|
|
94
|
+
if (url.pathname === '/api/evidence') {
|
|
95
|
+
const taskId = url.searchParams.get('taskId')?.trim();
|
|
96
|
+
if (!taskId) {
|
|
97
|
+
return jsonResponse({
|
|
98
|
+
schemaVersion: 'hadara.dashboard.api.error.v1',
|
|
99
|
+
command: 'dashboard.api',
|
|
100
|
+
ok: false,
|
|
101
|
+
issues: [
|
|
102
|
+
{
|
|
103
|
+
severity: 'error',
|
|
104
|
+
code: 'TASK_ID_REQUIRED',
|
|
105
|
+
message: 'Missing required query parameter: taskId.'
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}, headOnly, 400);
|
|
109
|
+
}
|
|
110
|
+
return jsonResponse((0, evidence_list_1.createEvidenceListReport)(projectRoot, { taskId }), headOnly);
|
|
111
|
+
}
|
|
112
|
+
return notFound();
|
|
113
|
+
}
|
|
114
|
+
function safePathname(requestUrl) {
|
|
115
|
+
const url = safeUrl(requestUrl);
|
|
116
|
+
return url?.pathname ?? null;
|
|
117
|
+
}
|
|
118
|
+
function safeUrl(requestUrl) {
|
|
119
|
+
if (/(^|\/)\.\.?($|[/?#])|%2e|%2f|\\/i.test(requestUrl))
|
|
120
|
+
return null;
|
|
121
|
+
try {
|
|
122
|
+
return new URL(requestUrl, 'http://hadara.local');
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function fileResponse(projectRoot, relativePath, contentType, headOnly) {
|
|
129
|
+
const filePath = node_path_1.default.join(projectRoot, relativePath);
|
|
130
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
131
|
+
return notFound();
|
|
132
|
+
const body = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
133
|
+
return {
|
|
134
|
+
statusCode: 200,
|
|
135
|
+
headers: securityHeaders({
|
|
136
|
+
'content-type': contentType,
|
|
137
|
+
'content-length': String(Buffer.byteLength(body))
|
|
138
|
+
}),
|
|
139
|
+
body: headOnly ? '' : body
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function jsonResponse(bodyValue, headOnly, statusCode = 200) {
|
|
143
|
+
const body = `${JSON.stringify(bodyValue, null, 2)}\n`;
|
|
144
|
+
return {
|
|
145
|
+
statusCode,
|
|
146
|
+
headers: securityHeaders({
|
|
147
|
+
'content-type': 'application/json; charset=utf-8',
|
|
148
|
+
'content-length': String(Buffer.byteLength(body))
|
|
149
|
+
}),
|
|
150
|
+
body: headOnly ? '' : body
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function notFound() {
|
|
154
|
+
return {
|
|
155
|
+
statusCode: 404,
|
|
156
|
+
headers: securityHeaders({ 'content-type': 'text/plain; charset=utf-8' }),
|
|
157
|
+
body: 'Not found'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function methodNotAllowed() {
|
|
161
|
+
return {
|
|
162
|
+
statusCode: 405,
|
|
163
|
+
headers: securityHeaders({
|
|
164
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
165
|
+
allow: 'GET, HEAD'
|
|
166
|
+
}),
|
|
167
|
+
body: 'Method not allowed'
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function internalError() {
|
|
171
|
+
return {
|
|
172
|
+
statusCode: 500,
|
|
173
|
+
headers: securityHeaders({ 'content-type': 'text/plain; charset=utf-8' }),
|
|
174
|
+
body: 'Internal server error'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function securityHeaders(headers) {
|
|
178
|
+
return {
|
|
179
|
+
...headers,
|
|
180
|
+
'cache-control': 'no-store',
|
|
181
|
+
'content-security-policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self'; object-src 'none'; base-uri 'none'",
|
|
182
|
+
'referrer-policy': 'no-referrer',
|
|
183
|
+
'x-content-type-options': 'nosniff'
|
|
184
|
+
};
|
|
185
|
+
}
|
package/dist/cli/debt.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleDebtCommand = handleDebtCommand;
|
|
4
|
+
const operational_debt_1 = require("../services/operational-debt");
|
|
5
|
+
function handleDebtCommand(input) {
|
|
6
|
+
if (input.args[0] !== 'debt')
|
|
7
|
+
return false;
|
|
8
|
+
const sub = input.args[1];
|
|
9
|
+
if (sub === 'list') {
|
|
10
|
+
const report = (0, operational_debt_1.createOperationalDebtReport)(input.projectRoot);
|
|
11
|
+
if (input.jsonOutput) {
|
|
12
|
+
console.log(JSON.stringify(report, null, 2));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
for (const record of report.records) {
|
|
16
|
+
console.log(`${record.id} | ${record.severity} | ${record.status} | ${record.title}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (sub === 'show') {
|
|
22
|
+
const id = input.args[2];
|
|
23
|
+
if (!id)
|
|
24
|
+
throw new Error('debt show requires an id');
|
|
25
|
+
const report = (0, operational_debt_1.createOperationalDebtShowReport)(input.projectRoot, id);
|
|
26
|
+
if (input.jsonOutput) {
|
|
27
|
+
console.log(JSON.stringify(report, null, 2));
|
|
28
|
+
}
|
|
29
|
+
else if (report.record) {
|
|
30
|
+
console.log(`${report.record.id} | ${report.record.severity} | ${report.record.status} | ${report.record.title}`);
|
|
31
|
+
console.log(`targetCapability: ${report.record.targetCapability}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(`[HADARA] ${report.issues[0]?.message ?? `Operational debt record not found: ${id}`}`);
|
|
35
|
+
}
|
|
36
|
+
if (!report.ok)
|
|
37
|
+
process.exitCode = 6;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createDoctorReport = createDoctorReport;
|
|
7
|
+
exports.formatDoctorReport = formatDoctorReport;
|
|
8
|
+
exports.handleDoctorCommand = handleDoctorCommand;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
function createDoctorReport(paths, nodeVersion = process.version) {
|
|
11
|
+
const checks = [
|
|
12
|
+
pathCheck('docs', paths.projectDocsDir),
|
|
13
|
+
pathCheck('tasks', paths.projectTasksDir),
|
|
14
|
+
pathCheck('project-context', paths.projectHadaraDir)
|
|
15
|
+
];
|
|
16
|
+
return {
|
|
17
|
+
schemaVersion: 'hadara.doctor.v1',
|
|
18
|
+
command: 'doctor',
|
|
19
|
+
ok: checks.every((check) => check.status === 'ok'),
|
|
20
|
+
runtime: {
|
|
21
|
+
node: nodeVersion
|
|
22
|
+
},
|
|
23
|
+
paths: {
|
|
24
|
+
portableRoot: paths.portableRoot,
|
|
25
|
+
dataRoot: paths.dataRoot,
|
|
26
|
+
projectRoot: paths.projectRoot
|
|
27
|
+
},
|
|
28
|
+
checks
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function formatDoctorReport(report) {
|
|
32
|
+
return [
|
|
33
|
+
'[HADARA] Doctor',
|
|
34
|
+
`portableRoot: ${report.paths.portableRoot}`,
|
|
35
|
+
`dataRoot: ${report.paths.dataRoot}`,
|
|
36
|
+
`projectRoot: ${report.paths.projectRoot}`,
|
|
37
|
+
`Node: ${report.runtime.node}`,
|
|
38
|
+
...report.checks.map((check) => `${formatCheckLabel(check.id)}${check.status}`)
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
function handleDoctorCommand(input) {
|
|
42
|
+
const report = createDoctorReport(input.paths);
|
|
43
|
+
if (input.jsonOutput) {
|
|
44
|
+
console.log(JSON.stringify(report, null, 2));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(formatDoctorReport(report));
|
|
48
|
+
}
|
|
49
|
+
if (!report.ok)
|
|
50
|
+
process.exitCode = 7;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function pathCheck(id, targetPath) {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
status: node_fs_1.default.existsSync(targetPath) ? 'ok' : 'missing',
|
|
57
|
+
path: targetPath
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function formatCheckLabel(id) {
|
|
61
|
+
if (id === 'docs')
|
|
62
|
+
return 'docs/: ';
|
|
63
|
+
if (id === 'tasks')
|
|
64
|
+
return 'tasks/: ';
|
|
65
|
+
if (id === 'project-context')
|
|
66
|
+
return '.hadara/: ';
|
|
67
|
+
return `${id}: `;
|
|
68
|
+
}
|